package esm_test import ( "context" "os" "path/filepath" "strings" "testing" "time" "git.fsdpf.net/go/jscriptx/esm" ) // 脚本可以从 node_modules 引用公共库:裸模块名、scoped 包、.ts 入口都支持。 func TestNodeModules_公共库(t *testing.T) { e := newEngine(t, "testdata/app/src") ctx := context.Background() obj, err := e.New(ctx, "Resource/ResSchemaController") if err != nil { t.Fatalf("加载失败: %v", err) } defer obj.Close() got, err := obj.Call(ctx, "Describe", "widget", 12.5) if err != nil { t.Fatalf("调用失败: %v", err) } // upper 来自 tinylib、money 来自 @fsdpf/util(scoped + .ts)、ex 来自同目录相对 import if got != "WIDGET ¥12.50 stored:s" { t.Errorf("got = %#v", got) } } // node_modules 里的文件不能被当成入口。 func TestNodeModules_不算入口(t *testing.T) { l, err := esm.NewLoader("testdata/app/src") if err != nil { t.Fatal(err) } for _, n := range l.Names() { if strings.Contains(n, "node_modules") { t.Errorf("node_modules 里的文件被当成入口了: %s", n) } } } // 依赖包不参与版本计算:改了 node_modules 不会自动重编译,Rebuild 之后才生效。 // 这是有意的取舍——真实 npm 包成百上千个文件,每次取脚本都 stat 一遍太贵。 func TestNodeModules_不参与热更新(t *testing.T) { dir := t.TempDir() mod := filepath.Join(dir, "Mod") lib := filepath.Join(dir, "node_modules", "mylib") os.MkdirAll(mod, 0o755) os.MkdirAll(lib, 0o755) write := func(p, s string) { t.Helper() if err := os.WriteFile(p, []byte(s), 0o644); err != nil { t.Fatal(err) } } write(filepath.Join(lib, "package.json"), `{"name":"mylib","main":"index.js"}`) write(filepath.Join(lib, "index.js"), `export function hi() { return "lib-v1" }`) write(filepath.Join(mod, "C.ts"), `import { hi } from "mylib" export default class C { Get(): string { return hi() } }`) loader, err := esm.NewLoader(dir) if err != nil { t.Fatal(err) } e := newEngineWith(t, loader) ctx := context.Background() obj, _ := e.New(ctx, "Mod/C") if got, _ := obj.Call(ctx, "Get"); got != "lib-v1" { t.Fatalf("got = %#v", got) } obj.Close() time.Sleep(10 * time.Millisecond) write(filepath.Join(lib, "index.js"), `export function hi() { return "lib-v2" }`) // 改依赖包不触发重编译 obj2, _ := e.New(ctx, "Mod/C") if got, _ := obj2.Call(ctx, "Get"); got != "lib-v1" { t.Errorf("依赖包不该参与热更新, got %#v", got) } obj2.Close() // 显式 Rebuild 之后生效 if err := loader.Rebuild(); err != nil { t.Fatal(err) } e.Invalidate("Mod/C") obj3, err := e.New(ctx, "Mod/C") if err != nil { t.Fatal(err) } defer obj3.Close() if got, _ := obj3.Call(ctx, "Get"); got != "lib-v2" { t.Errorf("Rebuild 后应该生效, got %#v", got) } // 业务代码照常热更新 time.Sleep(10 * time.Millisecond) write(filepath.Join(mod, "C.ts"), `import { hi } from "mylib" export default class C { Get(): string { return hi() + "!" } }`) obj4, err := e.New(ctx, "Mod/C") if err != nil { t.Fatal(err) } defer obj4.Close() if got, _ := obj4.Call(ctx, "Get"); got != "lib-v2!" { t.Errorf("业务代码该正常热更新, got %#v", got) } } // 公共库可以放在加载目录之外,用 WithNodePaths 指过去。 func TestNodeModules_指定目录(t *testing.T) { root := t.TempDir() app := filepath.Join(root, "app", "Mod") libs := filepath.Join(root, "shared-libs", "tinylib") os.MkdirAll(app, 0o755) os.MkdirAll(libs, 0o755) write := func(p, s string) { t.Helper() if err := os.WriteFile(p, []byte(s), 0o644); err != nil { t.Fatal(err) } } write(filepath.Join(libs, "package.json"), `{"name":"tinylib","main":"index.js"}`) write(filepath.Join(libs, "index.js"), `export function hi() { return "来自共享目录" }`) write(filepath.Join(app, "C.ts"), `import { hi } from "tinylib" export default class C { Get(): string { return hi() } }`) appDir := filepath.Join(root, "app") // 不指定时找不到:app 及其上层都没有 node_modules if _, err := esm.NewLoader(appDir); err == nil { t.Error("app 下没有 node_modules,不指定应该解析失败") } // 指过去就能用 loader, err := esm.NewLoader(appDir, esm.WithNodePaths(filepath.Join(root, "shared-libs"))) if err != nil { t.Fatalf("WithNodePaths 没生效: %v", err) } e := newEngineWith(t, loader) obj, err := e.New(context.Background(), "Mod/C") if err != nil { t.Fatal(err) } defer obj.Close() if got, _ := obj.Call(context.Background(), "Get"); got != "来自共享目录" { t.Errorf("got = %#v", got) } } // Alias 把模块名钉到具体文件上,绕过 node_modules 查找。 func TestNodeModules_Alias(t *testing.T) { root := t.TempDir() app := filepath.Join(root, "app", "Mod") os.MkdirAll(app, 0o755) os.MkdirAll(filepath.Join(root, "impl"), 0o755) os.WriteFile(filepath.Join(root, "impl", "mylib.ts"), []byte(`export function hi(): string { return "来自 alias" }`), 0o644) os.WriteFile(filepath.Join(app, "C.ts"), []byte(`import { hi } from "tinylib" export default class C { Get(): string { return hi() } }`), 0o644) loader, err := esm.NewLoader(filepath.Join(root, "app"), esm.WithAlias(map[string]string{ "tinylib": filepath.Join(root, "impl", "mylib.ts"), })) if err != nil { t.Fatalf("WithAlias 没生效: %v", err) } e := newEngineWith(t, loader) obj, err := e.New(context.Background(), "Mod/C") if err != nil { t.Fatal(err) } defer obj.Close() if got, _ := obj.Call(context.Background(), "Get"); got != "来自 alias" { t.Errorf("got = %#v", got) } }