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:
+229
@@ -0,0 +1,229 @@
|
||||
package main
|
||||
|
||||
// AST 扫描:在用户模块里找 BindService / BindServiceForGen 调用点,
|
||||
// 按目录聚合——fun 框架零改动,采集全靠 fun 公开 API
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type serviceRef struct {
|
||||
Alias string // &pkg.Svc 的 pkg 别名;同包引用为空
|
||||
Import string // pkg 对应的导入路径
|
||||
TypeName string
|
||||
}
|
||||
|
||||
// modulePath 读 go.mod 的 module 行
|
||||
func modulePath(root string) (string, error) {
|
||||
data, err := os.ReadFile(filepath.Join(root, "go.mod"))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("找不到 go.mod(%s): %w", root, err)
|
||||
}
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "module ") {
|
||||
return strings.TrimSpace(strings.TrimPrefix(line, "module ")), nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("go.mod 中没有 module 声明")
|
||||
}
|
||||
|
||||
// findModuleRoot 从 dir 向上找 go.mod
|
||||
func findModuleRoot(dir string) (string, error) {
|
||||
abs, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for {
|
||||
if _, err := os.Stat(filepath.Join(abs, "go.mod")); err == nil {
|
||||
return abs, nil
|
||||
}
|
||||
parent := filepath.Dir(abs)
|
||||
if parent == abs {
|
||||
return "", fmt.Errorf("从 %s 向上未找到 go.mod", dir)
|
||||
}
|
||||
abs = parent
|
||||
}
|
||||
}
|
||||
|
||||
var scanSkipDirs = map[string]bool{
|
||||
".git": true, "vendor": true, "node_modules": true, ".fun-tmp": true, "testdata": true,
|
||||
}
|
||||
|
||||
// scanBindTargets 扫描模块内全部 .go 文件的 BindService* 调用,
|
||||
// 按目录聚合成待生成辅助测试的包目标(测试文件可访问包内类型,
|
||||
// 服务定义在 package main 也能采集)
|
||||
func scanBindTargets(root string) ([]helperTarget, error) {
|
||||
byDir := map[string]*helperTarget{}
|
||||
seen := map[string]bool{}
|
||||
|
||||
add := func(dir, pkgName string, r serviceRef) {
|
||||
key := dir + "|" + r.Alias + "|" + r.Import + "|" + r.TypeName
|
||||
if seen[key] {
|
||||
return
|
||||
}
|
||||
seen[key] = true
|
||||
t, ok := byDir[dir]
|
||||
if !ok {
|
||||
t = &helperTarget{Dir: dir, Pkg: pkgName}
|
||||
byDir[dir] = t
|
||||
}
|
||||
t.Refs = append(t.Refs, r)
|
||||
}
|
||||
|
||||
err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if d.IsDir() {
|
||||
if path != root && (scanSkipDirs[d.Name()] || strings.HasPrefix(d.Name(), ".")) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
||||
return nil
|
||||
}
|
||||
fset := token.NewFileSet()
|
||||
file, perr := parser.ParseFile(fset, path, nil, 0)
|
||||
if perr != nil {
|
||||
return nil // 语法不过的文件(生成物/草稿)跳过
|
||||
}
|
||||
|
||||
// 该文件可见的导入:别名/包名 → 导入路径
|
||||
imports := map[string]string{}
|
||||
for _, imp := range file.Imports {
|
||||
importPath, _ := strconv.Unquote(imp.Path.Value)
|
||||
alias := filepath.Base(importPath)
|
||||
if imp.Name != nil {
|
||||
alias = imp.Name.Name
|
||||
}
|
||||
imports[alias] = importPath
|
||||
}
|
||||
|
||||
// 服务列表变量:for _, x := range []any{&A{}, &B{}} / x := []any{...}
|
||||
// (BindServiceForGen(svc) 循环注册的常见写法)
|
||||
parsePtrComposite := func(e ast.Expr) (serviceRef, bool) {
|
||||
u, ok := e.(*ast.UnaryExpr)
|
||||
if !ok || u.Op != token.AND {
|
||||
return serviceRef{}, false
|
||||
}
|
||||
lit, ok := u.X.(*ast.CompositeLit)
|
||||
if !ok {
|
||||
return serviceRef{}, false
|
||||
}
|
||||
switch t := lit.Type.(type) {
|
||||
case *ast.Ident:
|
||||
return serviceRef{TypeName: t.Name}, true
|
||||
case *ast.SelectorExpr:
|
||||
if pkg, ok := t.X.(*ast.Ident); ok {
|
||||
if importPath, ok := imports[pkg.Name]; ok {
|
||||
return serviceRef{Alias: pkg.Name, Import: importPath, TypeName: t.Sel.Name}, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return serviceRef{}, false
|
||||
}
|
||||
collectSlice := func(lit *ast.CompositeLit) []serviceRef {
|
||||
var refs []serviceRef
|
||||
for _, el := range lit.Elts {
|
||||
if r, ok := parsePtrComposite(el); ok {
|
||||
refs = append(refs, r)
|
||||
}
|
||||
}
|
||||
return refs
|
||||
}
|
||||
sliceVars := map[string][]serviceRef{}
|
||||
var collectVars func(n ast.Node) bool
|
||||
collectVars = func(n ast.Node) bool {
|
||||
switch st := n.(type) {
|
||||
case *ast.RangeStmt:
|
||||
if v, ok := st.Value.(*ast.Ident); ok {
|
||||
if lit, ok := st.X.(*ast.CompositeLit); ok {
|
||||
sliceVars[v.Name] = collectSlice(lit)
|
||||
}
|
||||
}
|
||||
case *ast.AssignStmt:
|
||||
if len(st.Lhs) == 1 {
|
||||
if id, ok := st.Lhs[0].(*ast.Ident); ok {
|
||||
if lit, ok := st.Rhs[0].(*ast.CompositeLit); ok {
|
||||
sliceVars[id.Name] = collectSlice(lit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
ast.Inspect(file, collectVars)
|
||||
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
call, ok := n.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
sel, ok := call.Fun.(*ast.SelectorExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if sel.Sel.Name != "BindService" && sel.Sel.Name != "BindServiceForGen" {
|
||||
return true
|
||||
}
|
||||
if len(call.Args) == 0 {
|
||||
return true
|
||||
}
|
||||
// 参数形如 &pkg.Svc{} / &Svc{}
|
||||
var typeExpr ast.Expr
|
||||
if a, ok := call.Args[0].(*ast.UnaryExpr); ok && a.Op == token.AND {
|
||||
if lit, ok := a.X.(*ast.CompositeLit); ok {
|
||||
typeExpr = lit.Type
|
||||
}
|
||||
}
|
||||
if ident, ok := call.Args[0].(*ast.Ident); ok {
|
||||
// BindServiceForGen(svc):循环/切片变量注册
|
||||
for _, r := range sliceVars[ident.Name] {
|
||||
add(filepath.Dir(path), file.Name.Name, r)
|
||||
}
|
||||
return true
|
||||
}
|
||||
if typeExpr == nil {
|
||||
return true
|
||||
}
|
||||
switch t := typeExpr.(type) {
|
||||
case *ast.Ident:
|
||||
add(filepath.Dir(path), file.Name.Name, serviceRef{TypeName: t.Name})
|
||||
case *ast.SelectorExpr:
|
||||
if pkg, ok := t.X.(*ast.Ident); ok {
|
||||
if importPath, ok := imports[pkg.Name]; ok {
|
||||
add(filepath.Dir(path), file.Name.Name, serviceRef{
|
||||
Alias: pkg.Name, Import: importPath, TypeName: t.Sel.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dirs := make([]string, 0, len(byDir))
|
||||
for d := range byDir {
|
||||
dirs = append(dirs, d)
|
||||
}
|
||||
sort.Strings(dirs)
|
||||
targets := make([]helperTarget, 0, len(dirs))
|
||||
for _, d := range dirs {
|
||||
targets = append(targets, *byDir[d])
|
||||
}
|
||||
return targets, nil
|
||||
}
|
||||
Reference in New Issue
Block a user