package esm_test import ( "context" "encoding/json" "os" "path/filepath" "strings" "testing" "git.fsdpf.net/go/jscriptx" "git.fsdpf.net/go/jscriptx/esm" ) // writePkg 在 dir/node_modules/name 下摆一个包。 func writePkg(t *testing.T, dir, name, version, entry, code string, deps map[string]string) { t.Helper() p := filepath.Join(dir, "node_modules", filepath.FromSlash(name)) if err := os.MkdirAll(p, 0o755); err != nil { t.Fatal(err) } pkg := map[string]any{"name": name, "version": version, "type": "module", "main": entry} if len(deps) > 0 { pkg["dependencies"] = deps } b, _ := json.Marshal(pkg) if err := os.WriteFile(filepath.Join(p, "package.json"), b, 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(p, entry), []byte(code), 0o644); err != nil { t.Fatal(err) } } // 一棵装好的依赖树:主包 + 它依赖的包,两个都在 node_modules 顶层(npm 的平铺布局)。 func installedTree(t *testing.T) string { t.Helper() dir := t.TempDir() writePkg(t, dir, "dep-lib", "2.0.0", "index.js", `export function helper(x) { return "dep:" + x }`, nil) writePkg(t, dir, "main-lib", "1.0.0", "index.js", ` import { helper } from "dep-lib" export function greet(x) { return helper(x) + "!" } export default { greet } `, map[string]string{"dep-lib": "^2.0.0"}) return dir } // 摊平之后,产物里不能再有对依赖的 import——依赖得被内联进去。 func TestVendor_依赖被打进单文件(t *testing.T) { from := installedTree(t) out := t.TempDir() res, err := esm.Vendor(from, "main-lib", out) if err != nil { t.Fatal(err) } if res.Version != "1.0.0" { t.Errorf("版本 = %q", res.Version) } // 转发入口 + main-lib + dep-lib if res.Modules < 2 { t.Errorf("只打进 %d 个文件,依赖没被拉进来?", res.Modules) } b, err := os.ReadFile(filepath.Join(res.Dir, "index.mjs")) if err != nil { t.Fatal(err) } src := string(b) if strings.Contains(src, `"dep-lib"`) || strings.Contains(src, `'dep-lib'`) { t.Errorf("产物里还留着对 dep-lib 的引用,没真正内联:\n%s", src) } if !strings.Contains(src, "dep:") { t.Errorf("产物里没有依赖的代码:\n%s", src) } } // 摊平出来的东西要能直接被 Loader 用,脚本的 import 写法不变。 func TestVendor_摊平后脚本能直接用(t *testing.T) { from := installedTree(t) app := t.TempDir() if _, err := esm.Vendor(from, "main-lib", filepath.Join(app, "node_modules")); err != nil { t.Fatal(err) } if err := os.MkdirAll(filepath.Join(app, "Http"), 0o755); err != nil { t.Fatal(err) } // 具名导出和默认导出都试一下 if err := os.WriteFile(filepath.Join(app, "Http", "C.ts"), []byte(` import lib, { greet } from "main-lib" export default class C { Run(): string { return greet("a") + "/" + lib.greet("b") } }`), 0o644); err != nil { t.Fatal(err) } loader, err := esm.NewLoader(app) if err != nil { t.Fatalf("打包失败: %v", err) } e, err := jscriptx.New(jscriptx.WithLoader(loader), jscriptx.WithLogger(nil)) if err != nil { t.Fatal(err) } defer e.Close() s, err := e.Script("Http/C") if err != nil { t.Fatal(err) } ctx := context.Background() obj, err := s.New(ctx) if err != nil { t.Fatal(err) } defer obj.Close() got, err := obj.Call(ctx, "Run") if err != nil { t.Fatal(err) } if got != "dep:a!/dep:b!" { t.Errorf("Run = %#v", got) } } // 只有具名导出的包(ESM 原生的很多是这样)也要能摊平。 // // 转发源码里写 `export { default }` 会让 esbuild 报错,Vendor 得能退回去重打。 func TestVendor_没有默认导出也能摊平(t *testing.T) { dir := t.TempDir() writePkg(t, dir, "named-only", "1.0.0", "index.js", `export const answer = 42`, nil) res, err := esm.Vendor(dir, "named-only", t.TempDir()) if err != nil { t.Fatalf("没有默认导出就摊不平了: %v", err) } b, _ := os.ReadFile(filepath.Join(res.Dir, "index.mjs")) if !strings.Contains(string(b), "42") { t.Errorf("产物不对: %s", b) } } // 包没装就要给一句说得清的错,而不是一个 file not found。 func TestVendor_包没装时说清楚该怎么办(t *testing.T) { _, err := esm.Vendor(t.TempDir(), "not-installed", t.TempDir()) if err == nil { t.Fatal("该报错") } for _, want := range []string{"not-installed", "npm i"} { if !strings.Contains(err.Error(), want) { t.Errorf("错误里该提到 %q: %v", want, err) } } } // 写出来的 package.json 是给解析器找入口用的,少了它整个就不成立。 func TestVendor_写出最小的package_json(t *testing.T) { res, err := esm.Vendor(installedTree(t), "main-lib", t.TempDir()) if err != nil { t.Fatal(err) } b, err := os.ReadFile(filepath.Join(res.Dir, "package.json")) if err != nil { t.Fatal(err) } var pkg map[string]any if err := json.Unmarshal(b, &pkg); err != nil { t.Fatal(err) } for k, want := range map[string]any{ "name": "main-lib", "version": "1.0.0", "module": "index.mjs", "main": "index.mjs", } { if pkg[k] != want { t.Errorf("package.json[%s] = %#v, want %#v", k, pkg[k], want) } } // 有 exports 的话解析器只认里面列的子路径,而摊平后子路径已经不存在了 if _, ok := pkg["exports"]; ok { t.Error("不该写 exports 字段") } }