Files
jscriptx/esm/typings_test.go
what 6ce9b483fd feat(esm): 按目录加载脚本、打包与热更新
把一个目录当脚本仓库:按路径寻址、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 一遍太贵。
2026-09-05 22:13:57 +08:00

155 lines
4.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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("落盘的类型跟打包用的对不上")
}
}