除了常规用例,有几组是专门钉住「引擎的既定事实」的,改动时会先红:
- vmstate_test goja 的硬限制:非原始值不是 goroutine 安全的、对象跨不了
Runtime。其中一条是哨兵——goja 哪天允许对象跨 Runtime 了,
它会失败,提醒我们可以简化设计。
- govalue_test Go 的切片/map 进到脚本里长什么样。这不是本库的行为而是
goja 的,但脚本作者照着它写代码,变了会静默走错分支。
- safety_test 并发下坏 VM 不会被别的 goroutine 捡到。
- bench_gobind 脚本碰 Go 对象的单次开销,README 性能一节的数据来源。
missing_global_test 里有一条断言 typeof setTimeout === "undefined":
定时器必须保持未定义,库里的特性探测才能正常降级,防止以后有人把桩加回来。
159 lines
4.6 KiB
Go
159 lines
4.6 KiB
Go
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)
|
|
}
|