package esm_test import ( "context" "errors" "fmt" "os" "path/filepath" "strings" "testing" "time" "git.fsdpf.net/go/jscriptx" "git.fsdpf.net/go/jscriptx/esm" ) // 两个 esm.Loader 叠起来:入口合并,同名的后面盖前面。 func TestOverlay_两个目录合并与覆盖(t *testing.T) { base, err := esm.NewLoader("testdata/app/src") if err != nil { t.Fatal(err) } over, err := esm.NewLoader("testdata/app2/src") if err != nil { t.Fatal(err) } e := newEngineWith(t, base, over) ctx := context.Background() // 同名的取后面那层 obj, err := e.New(ctx, "Resource/ResCreateController") if err != nil { t.Fatal(err) } defer obj.Close() if got, _ := obj.Call(ctx, "Init"); got != "来自 app2 的覆盖实现" { t.Errorf("覆盖没生效: %#v", got) } // 前面那层独有的照常能用 obj2, err := e.New(ctx, "PkgVersion/PkgImportController", "framework") if err != nil { t.Fatal(err) } defer obj2.Close() if got, _ := obj2.Call(ctx, "Init"); got != "PkgImport:framework" { t.Errorf("前面那层的入口坏了: %#v", got) } } // 每层各自往上找自己的 node_modules,依赖互不干扰。 func TestOverlay_各层独立解析依赖(t *testing.T) { base, _ := esm.NewLoader("testdata/app/src") over, _ := esm.NewLoader("testdata/app2/src") e := newEngineWith(t, base, over) ctx := context.Background() // app2 的入口用 app2/node_modules 里的包 obj, err := e.New(ctx, "Extra/ExtraController") if err != nil { t.Fatal(err) } defer obj.Close() if got, _ := obj.Call(ctx, "Who"); got != "app2 独有:app2 专属依赖" { t.Errorf("app2 的依赖没解析对: %#v", got) } // app 的入口用 app/node_modules 里的包,两边互不影响 obj2, err := e.New(ctx, "Resource/ResSchemaController") if err != nil { t.Fatal(err) } defer obj2.Close() if got, _ := obj2.Call(ctx, "Describe", "widget", 12.5); got != "WIDGET ¥12.50 stored:s" { t.Errorf("app 的依赖被 app2 影响了: %#v", got) } } // 每层可以有完全不同的配置——这里给两层不同的入口规则。 func TestOverlay_各层配置独立(t *testing.T) { d1, d2 := t.TempDir(), t.TempDir() write(t, d1, "Mod/A.ts", `export default class A { V(): string { return "a" } }`) write(t, d2, "Mod/B.ts", `export default class B { V(): string { return "b" } }`) write(t, d2, "Mod/Skip.ts", `export default class S { V(): string { return "s" } }`) base, _ := esm.NewLoader(d1) // 只认 B 开头的入口,Skip.ts 不算 over, err := esm.NewLoader(d2, esm.WithGlobs("*/B*.ts")) if err != nil { t.Fatal(err) } e := newEngineWith(t, base, over) if _, err := e.New(context.Background(), "Mod/B"); err != nil { t.Errorf("Mod/B 该能取到: %v", err) } if _, err := e.New(context.Background(), "Mod/Skip"); err == nil { t.Error("Mod/Skip 不符合那层的入口规则,不该被收进来") } } // 覆盖层的文件删掉后,被盖住的实现要能恢复。 func TestOverlay_覆盖撤销后恢复(t *testing.T) { d1, d2 := t.TempDir(), t.TempDir() write(t, d1, "Mod/C.ts", `export default class C { Who(): string { return "base" } }`) write(t, d2, "Mod/C.ts", `export default class C { Who(): string { return "override" } }`) base, _ := esm.NewLoader(d1) over, _ := esm.NewLoader(d2) e := newEngineWith(t, base, over) call := func() string { t.Helper() obj, err := e.New(context.Background(), "Mod/C") if err != nil { t.Fatal(err) } defer obj.Close() got, err := obj.Call(context.Background(), "Who") if err != nil { t.Fatal(err) } return got.(string) } if got := call(); got != "override" { t.Fatalf("覆盖没生效: %q", got) } time.Sleep(10 * time.Millisecond) os.Remove(filepath.Join(d2, "Mod/C.ts")) if err := over.Rebuild(); err != nil { t.Fatal(err) } if got := call(); got != "base" { t.Errorf("撤销覆盖后没落回 base: %q", got) } } // 热更新对每一层都生效。 func TestOverlay_热更新(t *testing.T) { d1, d2 := t.TempDir(), t.TempDir() write(t, d1, "A/X.ts", `export default class X { V(): string { return "a-v1" } }`) write(t, d2, "B/Y.ts", `export default class Y { V(): string { return "b-v1" } }`) base, _ := esm.NewLoader(d1) over, _ := esm.NewLoader(d2) e := newEngineWith(t, base, over) call := func(name string) string { t.Helper() obj, err := e.New(context.Background(), name) if err != nil { t.Fatal(err) } defer obj.Close() got, _ := obj.Call(context.Background(), "V") return got.(string) } if call("A/X") != "a-v1" || call("B/Y") != "b-v1" { t.Fatal("初始状态不对") } time.Sleep(10 * time.Millisecond) write(t, d2, "B/Y.ts", `export default class Y { V(): string { return "b-v2" } }`) if got := call("B/Y"); got != "b-v2" { t.Errorf("上层的热更新没生效: %q", got) } time.Sleep(10 * time.Millisecond) write(t, d1, "A/X.ts", `export default class X { V(): string { return "a-v2" } }`) if got := call("A/X"); got != "a-v2" { t.Errorf("下层的热更新没生效: %q", got) } } // 覆盖层新增一个同名入口,要能盖住已经在跑的下层实现。 func TestOverlay_运行中新增覆盖(t *testing.T) { d1, d2 := t.TempDir(), t.TempDir() write(t, d1, "Mod/D.ts", `export default class D { V(): string { return "base" } }`) base, _ := esm.NewLoader(d1) over, _ := esm.NewLoader(d2) // 一开始是空的 e := newEngineWith(t, base, over) call := func() string { t.Helper() obj, err := e.New(context.Background(), "Mod/D") if err != nil { t.Fatal(err) } defer obj.Close() got, _ := obj.Call(context.Background(), "V") return got.(string) } if got := call(); got != "base" { t.Fatalf("初始该是 base: %q", got) } time.Sleep(10 * time.Millisecond) write(t, d2, "Mod/D.ts", `export default class D { V(): string { return "patched" } }`) if err := over.Rebuild(); err != nil { t.Fatal(err) } if got := call(); got != "patched" { t.Errorf("新增的覆盖没生效: %q", got) } } // Overlay 汇总各层的入口名。 func TestOverlay_Names汇总(t *testing.T) { base, _ := esm.NewLoader("testdata/app/src") over, _ := esm.NewLoader("testdata/app2/src") o, err := jscriptx.Overlay(base, over) if err != nil { t.Fatal(err) } names := strings.Join(o.Names(), " ") for _, want := range []string{"PkgVersion/PkgImportController", "Extra/ExtraController"} { if !strings.Contains(names, want) { t.Errorf("Names() 里少了 %s: %s", want, names) } } // 同名只算一次 if n := strings.Count(names, "Resource/ResCreateController"); n != 1 { t.Errorf("同名入口出现了 %d 次: %s", n, names) } } func write(t *testing.T, dir, rel, content string) { t.Helper() p := filepath.Join(dir, rel) if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(p, []byte(content), 0o644); err != nil { t.Fatal(err) } } // README 里推荐的混合来源写法:磁盘目录一层 + 自定义来源一层, // 后者用 PreparedFunc + Bundle 自己打包,好跟 esm.Loader 的 Prepared 对齐。 func TestOverlay_磁盘层加自定义来源层(t *testing.T) { disk, err := esm.NewLoader("testdata/app/src") if err != nil { t.Fatal(err) } // 假装是数据库:只存了一份覆盖脚本 rows := map[string]string{ "Resource/ResCreateController": `export default class C { Init(): string { return "来自数据库" } }`, } db := jscriptx.PreparedFunc(func(name string) (string, string, error) { src, ok := rows[name] if !ok { return "", "", fmt.Errorf("%w: %s", jscriptx.ErrScriptNotFound, name) } bundled, err := jscriptx.Bundle(name+".ts", src) if err != nil { return "", "", err } return bundled, "v1", nil }) e, err := jscriptx.New(jscriptx.WithLogger(nil), jscriptx.WithLoader(disk, db)) if err != nil { t.Fatal(err) } defer e.Close() ctx := context.Background() // 数据库那层盖住了磁盘上的同名脚本 obj, err := e.New(ctx, "Resource/ResCreateController") if err != nil { t.Fatal(err) } defer obj.Close() if got, _ := obj.Call(ctx, "Init"); got != "来自数据库" { t.Errorf("数据库那层没盖住磁盘的: %#v", got) } // 数据库里没有的照样落回磁盘那层 obj2, err := e.New(ctx, "PkgVersion/PkgImportController", "framework") if err != nil { t.Fatal(err) } defer obj2.Close() if got, _ := obj2.Call(ctx, "Init"); got != "PkgImport:framework" { t.Errorf("没落回磁盘那层: %#v", got) } } // 两个版本的脚本目录,调用时点名要哪个版本。 func TestVersion_两个版本的目录(t *testing.T) { d1, d2 := t.TempDir(), t.TempDir() write(t, d1, "Resource/ResCreateController.ts", `export default class C { Init(): string { return "v1 的实现" } }`) write(t, d1, "Legacy/OldController.ts", `export default class O { Init(): string { return "只有 v1 有" } }`) write(t, d2, "Resource/ResCreateController.ts", `export default class C { Init(): string { return "v2 的实现" } }`) v1, err := esm.NewLoader(d1, esm.WithVersion("v1")) if err != nil { t.Fatal(err) } v2, err := esm.NewLoader(d2, esm.WithVersion("v2")) if err != nil { t.Fatal(err) } if v1.Version() != "v1" { t.Errorf("Version() = %q", v1.Version()) } e := newEngineWith(t, v1, v2) ctx := context.Background() call := func(name string) string { t.Helper() obj, err := e.New(ctx, name) if err != nil { t.Fatal(err) } defer obj.Close() got, err := obj.Call(ctx, "Init") if err != nil { t.Fatal(err) } return got.(string) } if got := call("Resource/ResCreateController"); got != "v2 的实现" { t.Errorf("不点名该拿到最上层: %q", got) } if got := call("Resource/ResCreateController@v1"); got != "v1 的实现" { t.Errorf("点名 v1: %q", got) } if got := call("Resource/ResCreateController@v2"); got != "v2 的实现" { t.Errorf("点名 v2: %q", got) } if got := call("Legacy/OldController@v1"); got != "只有 v1 有" { t.Errorf("点名 v1 独有的: %q", got) } // v2 层没有 Legacy/OldController,点名它就该报不存在,不能回落到 v1 if _, err := e.New(ctx, "Legacy/OldController@v2"); !errors.Is(err, jscriptx.ErrScriptNotFound) { t.Errorf("点名 v2 该报不存在: %v", err) } } // 点名各版本后,各自的热更新互不干扰。 func TestVersion_各版本独立热更新(t *testing.T) { d1, d2 := t.TempDir(), t.TempDir() write(t, d1, "Mod/A.ts", `export default class A { V(): string { return "v1-a" } }`) write(t, d2, "Mod/A.ts", `export default class A { V(): string { return "v2-a" } }`) v1, _ := esm.NewLoader(d1, esm.WithVersion("v1")) v2, _ := esm.NewLoader(d2, esm.WithVersion("v2")) e := newEngineWith(t, v1, v2) ctx := context.Background() call := func(name string) string { t.Helper() obj, err := e.New(ctx, name) if err != nil { t.Fatal(err) } defer obj.Close() got, _ := obj.Call(ctx, "V") return got.(string) } if call("Mod/A@v1") != "v1-a" || call("Mod/A@v2") != "v2-a" { t.Fatal("初始状态不对") } // 只改 v1 那份 time.Sleep(10 * time.Millisecond) write(t, d1, "Mod/A.ts", `export default class A { V(): string { return "v1-b" } }`) if got := call("Mod/A@v1"); got != "v1-b" { t.Errorf("v1 的热更新没生效: %q", got) } if got := call("Mod/A@v2"); got != "v2-a" { t.Errorf("改 v1 不该影响 v2: %q", got) } }