package jscriptx_test import ( "context" "errors" "path/filepath" "strings" "testing" "git.fsdpf.net/go/jscriptx" "github.com/evanw/esbuild/pkg/api" ) // WithBundleDefine:编译期常量替换。 func TestBundleOption_Define(t *testing.T) { e := newEngine(t, jscriptx.WithBundleOptions( jscriptx.WithBundleDefine(map[string]string{"__MODE__": `"生产"`}), )) s := mustCompile(t, e, "def.ts", ` declare const __MODE__: string export function mode(): string { return __MODE__ } `) got, err := s.Call(context.Background(), "mode") if err != nil { t.Fatal(err) } if got != "生产" { t.Errorf("常量没被替换, got %#v", got) } } // WithBundleTarget:把语法降级到指定的 ECMAScript 版本。 // goja 对新语法的覆盖不是 100%,调低 target 是遇到问题时的出路。 func TestBundleOption_Target(t *testing.T) { // 用可选链和空值合并这类较新的语法,降级到 ES2015 后应该被改写掉 src := `export function pick(o) { return o?.a?.b ?? "兜底" }` for _, target := range []struct { name string t api.Target }{{"ES2015", api.ES2015}, {"ES2020", api.ES2020}} { t.Run(target.name, func(t *testing.T) { e := newEngine(t, jscriptx.WithBundleOptions(jscriptx.WithBundleTarget(target.t))) s := mustCompile(t, e, "t.ts", src) ctx := context.Background() got, err := s.Call(ctx, "pick", map[string]any{"a": map[string]any{"b": "命中"}}) if err != nil { t.Fatalf("调用失败: %v", err) } if got != "命中" { t.Errorf("got = %#v", got) } if got, err := s.Call(ctx, "pick", nil); err != nil || got != "兜底" { t.Errorf("空值合并没生效: got=%#v err=%v", got, err) } }) } } // WithNodePaths / WithAlias:给直接 Compile 的源码指定模块来源。 func TestBundleOption_NodePathsAndAlias(t *testing.T) { libs, err := filepath.Abs("esm/testdata/app/node_modules") if err != nil { t.Fatal(err) } src := `import { upper } from "tinylib" export function shout(s: string): string { return upper(s) }` // 不指定时找不到:这段源码没有文件系统上下文 e0 := newEngine(t) if _, err := e0.Compile("x.ts", src); err == nil { t.Error("没有解析基准时应该报错") } // NodePaths 指向 node_modules 所在目录 e1 := newEngine(t, jscriptx.WithBundleOptions(jscriptx.WithNodePaths(libs))) s1, err := e1.Compile("x.ts", src) if err != nil { t.Fatalf("WithNodePaths 没生效: %v", err) } if got, _ := s1.Call(context.Background(), "shout", "hi"); got != "HI" { t.Errorf("got = %#v", got) } // Alias 把模块名钉到具体文件 impl, err := filepath.Abs("esm/testdata/app/node_modules/tinylib/index.js") if err != nil { t.Fatal(err) } e2 := newEngine(t, jscriptx.WithBundleOptions( jscriptx.WithAlias(map[string]string{"tinylib": impl}), )) s2, err := e2.Compile("x.ts", src) if err != nil { t.Fatalf("WithAlias 没生效: %v", err) } if got, _ := s2.Call(context.Background(), "shout", "hi"); got != "HI" { t.Errorf("got = %#v", got) } } // 直接 Compile 的源码不能有 import,报错要指出走 esm 子包。 func TestEntry_Compile不支持import(t *testing.T) { e := newEngine(t) _, err := e.Compile("imp.ts", `import x from "./other" export default class H { m() { return x } }`) if err == nil { t.Fatal("应该报错") } for _, want := range []string{"Could not resolve", "jscriptx/esm"} { if !strings.Contains(err.Error(), want) { t.Errorf("报错里没有 %q: %v", want, err) } } t.Logf("%v", err) } // 配了 WithResolveDir 之后,直接 Compile 的源码也能 import。 func TestEntry_ResolveDir(t *testing.T) { e := newEngine(t, jscriptx.WithBundleOptions( jscriptx.WithResolveDir("esm/testdata/app/src/Resource"), )) s, err := e.Compile("inline.ts", `import ResExecuter from "./ResExecuter" export default class H { m() { return new ResExecuter().Store("cfg") } }`) if err != nil { t.Fatalf("配了 resolveDir 应该能 import: %v", err) } ctx := context.Background() obj, err := s.New(ctx) if err != nil { t.Fatal(err) } defer obj.Close() if got, _ := obj.Call(ctx, "m"); got != "stored:cfg" { t.Errorf("got = %#v", got) } } // 语法错误在 Compile 阶段就报,带文件位置。 func TestEntry_语法错误(t *testing.T) { e := newEngine(t) _, err := e.Compile("bad.ts", `export default class { 语法错误 (((`) if err == nil { t.Fatal("应该报语法错误") } var jsErr *jscriptx.Error if !errors.As(err, &jsErr) || jsErr.Kind != jscriptx.KindCompile { t.Fatalf("错误分类不对: %v", err) } if !strings.Contains(err.Error(), "bad.ts") { t.Errorf("报错没带文件名: %v", err) } t.Logf("%v", err) }