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 一遍太贵。
This commit is contained in:
2026-09-05 22:13:57 +08:00
parent 7fb8d0f966
commit 6ce9b483fd
34 changed files with 2403 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
package esm
import (
"git.fsdpf.net/go/jscriptx"
"github.com/evanw/esbuild/pkg/api"
)
// extNamespace 是扩展模块在 esbuild 里的命名空间。这些模块不在磁盘上,
// 靠插件在解析阶段临时提供,所以要跟真实文件区分开。
const extNamespace = "jscriptx-ext"
// extensionPlugin 把扩展的 TS 模块变成可以 import 的虚拟模块:
//
// import store from "@jscriptx/store"
//
// 磁盘上没有这个文件,路径由扩展自己的 Module() 给出,源码也是它提供的。
// 模块本身只是个门面——把全局对象转发出来并附上类型声明,真正的实现在 Go 侧,
// 由 jscriptx 在建 VM 时注入。
//
// 拿不到扩展实例时(比如脚本 import 了但 ctx 里没带这个扩展),打包阶段不会报错,
// 因为模块只声明全局;真正缺东西要到运行时才暴露——这跟直接用全局变量的行为一致。
func extensionPlugin(exts []jscriptx.Extension) api.Plugin {
modules := make(map[string]string, len(exts))
for _, ext := range exts {
path, source := ext.Module()
if path == "" || source == "" {
continue
}
modules[path] = source
}
return api.Plugin{
Name: "jscriptx-extensions",
Setup: func(build api.PluginBuild) {
// 认领扩展的模块路径,交给下面的 OnLoad 提供内容
build.OnResolve(api.OnResolveOptions{Filter: `.*`}, func(args api.OnResolveArgs) (api.OnResolveResult, error) {
if _, ok := modules[args.Path]; !ok {
return api.OnResolveResult{}, nil // 不是扩展模块,让 esbuild 照常解析
}
return api.OnResolveResult{Path: args.Path, Namespace: extNamespace}, nil
})
build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: extNamespace},
func(args api.OnLoadArgs) (api.OnLoadResult, error) {
source := modules[args.Path]
return api.OnLoadResult{Contents: &source, Loader: api.LoaderTS}, nil
})
},
}
}