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() }