package esm_test import ( "encoding/json" "os" "path/filepath" "strings" "testing" "git.fsdpf.net/go/jscriptx/esm" ) // fakeExt 是个只提供模块的扩展。 type fakeExt struct { name, path, source string } func (e fakeExt) Name() string { return e.name } func (e fakeExt) Bindings() map[string]any { return nil } func (e fakeExt) Module() (string, string) { return e.path, e.source } var ( extA = fakeExt{"a", "@jscriptx/aaa", "export declare const a: number\n"} extB = fakeExt{"b", "@scope/deep/bbb", "export declare const b: string\n"} extC = fakeExt{"c", "", "没有模块路径,不该出现"} ) func TestTypings_取扩展的类型(t *testing.T) { loader, err := esm.NewLoader("testdata/app/src", esm.WithExtensions(extA, extB, extC)) if err != nil { t.Fatal(err) } got := loader.Typings() if len(got) != 2 { t.Fatalf("该有两个模块(没有路径的不算),拿到 %d 个: %v", len(got), got) } if got["@jscriptx/aaa"] != extA.source { t.Errorf("源码不对: %q", got["@jscriptx/aaa"]) } if _, ok := got[""]; ok { t.Error("没有模块路径的扩展不该进来") } } // 不建 Loader 也能取。 func TestTypings_包级函数(t *testing.T) { got := esm.Typings(extA, extB) if len(got) != 2 { t.Fatalf("拿到 %d 个", len(got)) } } // 写成 node_modules 的布局,编辑器就能解析 import 了。 func TestWriteTypings_落盘布局(t *testing.T) { dir := t.TempDir() loader, err := esm.NewLoader("testdata/app/src", esm.WithExtensions(extA, extB)) if err != nil { t.Fatal(err) } n, err := loader.WriteTypings(dir) if err != nil { t.Fatal(err) } if n != 2 { t.Errorf("该写两个,写了 %d 个", n) } // 带 scope 的路径要变成对应的多级目录 for _, tc := range []struct{ modPath, source string }{ {"@jscriptx/aaa", extA.source}, {"@scope/deep/bbb", extB.source}, } { base := filepath.Join(dir, filepath.FromSlash(tc.modPath)) dts, err := os.ReadFile(filepath.Join(base, "index.ts")) if err != nil { t.Fatalf("%s: %v", tc.modPath, err) } if string(dts) != tc.source { t.Errorf("%s 的类型内容不对: %q", tc.modPath, dts) } raw, err := os.ReadFile(filepath.Join(base, "package.json")) if err != nil { t.Fatalf("%s: %v", tc.modPath, err) } var pkg struct { Name string `json:"name"` Types string `json:"types"` Main string `json:"main"` } if err := json.Unmarshal(raw, &pkg); err != nil { t.Fatalf("%s 的 package.json 不是合法 JSON: %v", tc.modPath, err) } if pkg.Name != tc.modPath { t.Errorf("name = %q,该是模块路径", pkg.Name) } if pkg.Types != "index.ts" { t.Errorf("types = %q", pkg.Types) } if pkg.Main != "" { t.Error("不该有 main:运行期没有这个模块,指了会让人以为能 require") } } } // 重写一遍要能覆盖,不能因为文件已存在就失败。 func TestWriteTypings_可重复写(t *testing.T) { dir := t.TempDir() loader, _ := esm.NewLoader("testdata/app/src", esm.WithExtensions(extA)) if _, err := loader.WriteTypings(dir); err != nil { t.Fatal(err) } if _, err := loader.WriteTypings(dir); err != nil { t.Fatalf("重写该成功: %v", err) } } // 模块路径不能把文件写到 dir 外面去。 func TestWriteTypings_挡住越界路径(t *testing.T) { for _, bad := range []string{"../evil", "/etc/evil", "@x/../../evil", "a//b"} { dir := t.TempDir() loader, _ := esm.NewLoader("testdata/app/src", esm.WithExtensions(fakeExt{"x", bad, "export {}\n"})) if _, err := loader.WriteTypings(dir); err == nil { t.Errorf("模块路径 %q 该被挡下来", bad) } } } // 写出来的类型要跟打包时用的是同一份,不然编辑器不飘红但运行时挂。 func TestWriteTypings_跟打包用的是同一份(t *testing.T) { dir := t.TempDir() loader, err := esm.NewLoader("testdata/app/src", esm.WithExtensions(extA)) if err != nil { t.Fatal(err) } if _, err := loader.WriteTypings(dir); err != nil { t.Fatal(err) } dts, err := os.ReadFile(filepath.Join(dir, "@jscriptx", "aaa", "index.ts")) if err != nil { t.Fatal(err) } // Typings 是打包插件读的同一个来源(都走 Extension.Module) if strings.TrimSpace(string(dts)) != strings.TrimSpace(loader.Typings()["@jscriptx/aaa"]) { t.Error("落盘的类型跟打包用的对不上") } }