feat: fun-cli v1.0.0 — gen/run/docs 三命令,fun 框架零改动

- fun gen [ts|go]:AST 扫描 BindService* 调用,临时生成 go test 辅助采集,
  经 fun v1.3.7 公开 API 生成,产物与内置 GenCode 字节级一致(回归测试锁定)
- fun run:热更新开发循环,先编译成功再切换,失败保留旧进程
- fun docs:浅色标准文档站——参数表/State 添加字段/请求 JSON 可编辑/
  响应格式化/NDJSON 流式逐行展示,反向代理 /cell 同源无 CORS
- 元数据:TS 产物两遍解析重建(枚举/可空/流式工厂/嵌套引用全支持)
- 纯标准库实现,零第三方依赖
This commit is contained in:
2026-09-19 06:32:45 +08:00
commit e29767362a
21 changed files with 4148 additions and 0 deletions
+134
View File
@@ -0,0 +1,134 @@
package main
// 采集辅助:在 BindService 调用所在的包目录临时生成 fun_gen_helper_test.go
// (测试文件可访问包内类型,天然解决"服务定义在 package main"不可 import 的问题),
// go test 执行完毕后删除。fun 框架本体零改动,只用其公开 API
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
)
const helperTestFile = "fun_gen_helper_test.go"
const helperTestName = "TestFunGenHelper"
// helperTarget 一个待生成辅助测试的包目录
type helperTarget struct {
Dir string
Pkg string
Refs []serviceRef
}
type helperRef struct {
Alias string // 引用别名(&pkg.Svc 的 pkg);同包引用为空
Import string // 引用的导入路径
TypeName string
}
// runFunGen 扫描绑定调用,生成辅助测试并执行:
// lang: "ts" / "go" / "both";out 为 GenCode 输出目录(会被 fun 清空重建)
func runFunGen(moduleRoot, lang, out string) error {
targets, err := scanBindTargets(moduleRoot)
if err != nil {
return err
}
if len(targets) == 0 {
return fmt.Errorf("未在模块中找到 BindService/BindServiceForGen 调用。" +
"请确认服务注册代码,或用 -from 直接指定元数据 JSON 文件")
}
// 清理保险:先移除可能的历史残留
defer cleanupHelpers(targets)
cleanupHelpers(targets)
pkgs := make([]string, 0, len(targets))
for _, t := range targets {
if err := writeHelperTest(moduleRoot, t); err != nil {
return err
}
rel, err := filepath.Rel(moduleRoot, t.Dir)
if err != nil {
return err
}
pkg := "./" + filepath.ToSlash(rel)
pkgs = append(pkgs, pkg)
}
args := append([]string{"test", "-vet=off", "-count=1", "-run", "^" + helperTestName + "$"}, pkgs...)
cmd := exec.Command("go", args...)
cmd.Dir = moduleRoot
cmd.Env = append(os.Environ(), "FUN_OUT="+out, "FUN_LANG="+lang)
cmd.Stdout, cmd.Stderr = os.Stderr, os.Stderr
if err := cmd.Run(); err != nil {
cleanupHelpers(targets)
return fmt.Errorf("采集执行失败: %w", err)
}
return nil
}
func cleanupHelpers(targets []helperTarget) {
for _, t := range targets {
os.Remove(filepath.Join(t.Dir, helperTestFile))
}
}
func writeHelperTest(moduleRoot string, t helperTarget) error {
// 汇总需要的外部导入(去重,按路径排序)
imports := map[string]string{} // alias → path
for _, r := range t.Refs {
if r.Alias != "" {
imports[r.Alias] = r.Import
}
}
paths := make([]string, 0, len(imports))
for _, p := range imports {
paths = append(paths, p)
}
sort.Strings(paths)
aliasOf := func(path string) string {
for a, p := range imports {
if p == path {
return a
}
}
return ""
}
var imp, binds strings.Builder
imp.WriteString("\t\"os\"\n\t\"testing\"\n\n\t\"github.com/cyi-cc/fun\"\n")
for _, p := range paths {
fmt.Fprintf(&imp, "\t%s %q\n", aliasOf(p), p)
}
for _, r := range t.Refs {
if r.Alias != "" {
fmt.Fprintf(&binds, "\tf.BindServiceForGen(&%s.%s{})\n", r.Alias, r.TypeName)
} else {
fmt.Fprintf(&binds, "\tf.BindServiceForGen(&%s{})\n", r.TypeName)
}
}
src := fmt.Sprintf(`// fun-cli 临时采集辅助(自动生成,执行完即删除)
package %s
import (
%s)
func %s(t *testing.T) {
f := fun.New()
%s fun.SetOutput(os.Getenv("FUN_OUT"))
switch os.Getenv("FUN_LANG") {
case "ts":
fun.GenCode(fun.GenTs{})
case "go":
fun.GenCode(fun.GenGo{})
default:
fun.GenCode(fun.GenGo{}, fun.GenTs{})
}
}
`, t.Pkg, imp.String(), helperTestName, binds.String())
return os.WriteFile(filepath.Join(t.Dir, helperTestFile), []byte(src), 0o644)
}