把一个目录当脚本仓库:按路径寻址、esbuild 打包、内容变了自动重编。
- Loader 扫目录建索引,Load(name) 给出打好包的源码和版本号
- bundler esbuild 的封装。ESM 格式而不是 IIFE——IIFE 会附带一整套
CommonJS interop helper,每建一个 VM 都要重跑一遍
- plugin 把扩展的 TS 模块变成可以 import 的虚拟模块,磁盘上没有文件
- typings 把这些虚拟模块的类型按 node_modules 布局落盘,编辑器才认识
esbuild 原生实现了 Node 的模块解析,所以脚本能直接 import node_modules
里的第三方库。写出来的类型文件是 index.ts 而不是 index.d.ts:扩展给的是
真正的模块源码,里面可能带实现,声明文件里不允许有实现。
node_modules 不参与热更新的版本计算——依赖包是装出来的,改动总伴随显式的
安装动作,而真实的 npm 包动辄上千个文件,每次取脚本 stat 一遍太贵。
189 lines
5.5 KiB
Go
189 lines
5.5 KiB
Go
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)
|
||
}
|
||
}
|