把一个目录当脚本仓库:按路径寻址、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 一遍太贵。
155 lines
4.3 KiB
Go
155 lines
4.3 KiB
Go
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("落盘的类型跟打包用的对不上")
|
||
}
|
||
}
|