把一个目录当脚本仓库:按路径寻址、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 一遍太贵。
131 lines
4.5 KiB
Go
131 lines
4.5 KiB
Go
package esm
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"sort"
|
||
"strings"
|
||
|
||
"git.fsdpf.net/go/jscriptx"
|
||
)
|
||
|
||
// Typings 返回扩展提供的 TS 模块:import 路径 -> 源码。
|
||
//
|
||
// 打包时这些模块由插件凭空提供(见 plugin.go),磁盘上没有对应文件——
|
||
// 编辑器因此解析不了 `import store from "@jscriptx/store"`,写脚本时一路飘红。
|
||
// 拿到这份内容自己落盘或者走接口发出去,就能把类型补给编辑器。
|
||
//
|
||
// 落盘用 WriteTypings 更省事。
|
||
func (l *Loader) Typings() map[string]string {
|
||
out := make(map[string]string, len(l.exts))
|
||
for _, ext := range l.exts {
|
||
path, source := ext.Module()
|
||
if path == "" || source == "" {
|
||
continue
|
||
}
|
||
out[path] = source
|
||
}
|
||
return out
|
||
}
|
||
|
||
// WriteTypings 把扩展的 TS 模块按 node_modules 的布局写到 dir 下:
|
||
//
|
||
// dir/@jscriptx/store/package.json
|
||
// dir/@jscriptx/store/index.ts
|
||
//
|
||
// 写的是 index.ts 而**不是** index.d.ts:扩展给的是真正的模块源码,里面可能带
|
||
// 实现(比如 @jscriptx/sse 的 EventStream 基类,方法体要转发到注入的全局)。
|
||
// 声明文件里不允许有实现,落成 .d.ts 会让 tsc 报 TS1183。
|
||
//
|
||
// 编辑器和 tsc 按 Node 的规则往上找 node_modules,所以 dir 传脚本根目录旁边的
|
||
// node_modules,`import store from "@jscriptx/store"` 就不飘红了,也不用配
|
||
// tsconfig 的 paths:
|
||
//
|
||
// loader.WriteTypings("app/node_modules")
|
||
//
|
||
// 写出来的只是类型,运行期不会用到——真正的实现在 Go 侧,打包时由插件提供源码。
|
||
// 所以这些目录随时可以删掉重写,也不该提交进版本库。
|
||
//
|
||
// 已经存在的同名文件会被覆盖。返回写了几个模块。
|
||
func (l *Loader) WriteTypings(dir string) (int, error) {
|
||
return writeTypings(dir, l.Typings())
|
||
}
|
||
|
||
// WriteTypings 把一组扩展的类型落盘,不必先有 Loader。
|
||
//
|
||
// 用在只导类型、不跑脚本的地方——比如一条 dump-typings 命令:那时没有脚本目录,
|
||
// 也不该为了拿类型去建引擎。语义跟上面那个方法完全一样。
|
||
func WriteTypings(dir string, exts ...jscriptx.Extension) (int, error) {
|
||
return writeTypings(dir, Typings(exts...))
|
||
}
|
||
|
||
func writeTypings(dir string, typings map[string]string) (int, error) {
|
||
// 排一下序,出错信息和日志才稳定
|
||
paths := make([]string, 0, len(typings))
|
||
for p := range typings {
|
||
paths = append(paths, p)
|
||
}
|
||
sort.Strings(paths)
|
||
|
||
for _, modPath := range paths {
|
||
sub, err := modDir(modPath)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
target := filepath.Join(dir, sub)
|
||
if err := os.MkdirAll(target, 0o755); err != nil {
|
||
return 0, fmt.Errorf("jscriptx/esm: 建目录 %s 失败: %w", target, err)
|
||
}
|
||
files := map[string]string{
|
||
typingsFile: typings[modPath],
|
||
"package.json": packageJSON(modPath),
|
||
}
|
||
for name, content := range files {
|
||
p := filepath.Join(target, name)
|
||
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
|
||
return 0, fmt.Errorf("jscriptx/esm: 写 %s 失败: %w", p, err)
|
||
}
|
||
}
|
||
}
|
||
return len(paths), nil
|
||
}
|
||
|
||
// modDir 把模块路径变成相对目录,顺便挡住能跳出 dir 的路径。
|
||
func modDir(modPath string) (string, error) {
|
||
if modPath == "" || strings.HasPrefix(modPath, "/") || filepath.IsAbs(modPath) {
|
||
return "", fmt.Errorf("jscriptx/esm: 模块路径 %q 不能是绝对路径", modPath)
|
||
}
|
||
for _, seg := range strings.Split(modPath, "/") {
|
||
if seg == "" || seg == "." || seg == ".." {
|
||
return "", fmt.Errorf("jscriptx/esm: 模块路径 %q 里有 %q 这样的片段", modPath, seg)
|
||
}
|
||
}
|
||
return filepath.FromSlash(modPath), nil
|
||
}
|
||
|
||
// typingsFile 是每个模块落盘时的文件名。见 WriteTypings 里为什么不是 .d.ts。
|
||
const typingsFile = "index.ts"
|
||
|
||
// packageJSON 给类型包一个最小的 package.json。只指 types,不指 main——
|
||
// 运行期没有这个模块,指了 main 反而会让人以为能 require。
|
||
//
|
||
// 不能有 exports 字段:一旦有,Node 就只认里面列出的子路径,像
|
||
// @jscriptx/app/response 这种嵌在别的模块目录里的子路径会解析不到。
|
||
func packageJSON(modPath string) string {
|
||
return fmt.Sprintf(`{
|
||
"name": %q,
|
||
"version": "0.0.0",
|
||
"types": %q,
|
||
"private": true
|
||
}
|
||
`, modPath, typingsFile)
|
||
}
|
||
|
||
// Typings 返回一组扩展提供的 TS 模块,不必先有 Loader。
|
||
//
|
||
// 想在没建 Loader 的地方拿类型(比如一个只负责生成类型的命令)时用它。
|
||
func Typings(exts ...jscriptx.Extension) map[string]string {
|
||
return (&Loader{exts: exts}).Typings()
|
||
}
|