refactor: 入口移至 cmd/fun,go install 后命令名为 fun
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
package main
|
||||
|
||||
// 解析 fun 生成的 TypeScript 客户端产物,重建文档站所需的元数据。
|
||||
// 生成物即元数据:fun 自己的反射保证类型准确,CLI 只做只读解析
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
reServiceClass = regexp.MustCompile(`(?m)^export default class (\w+) \{`)
|
||||
reStreamMethod = regexp.MustCompile(`(?m)^ async (\w+)\((?:dto:\s?([^,]+), )?onMessage: \(data: (.+?)\) => unknown`)
|
||||
reDataMethod = regexp.MustCompile(`(?m)^ async (\w+)\((?:dto:\s?([^,]+), )?options\?: RequestOptions\): Promise<result<(.+?)>>`)
|
||||
reInterface = regexp.MustCompile(`(?s)export default interface (\w+) \{([^}]*)\}`)
|
||||
reEnum = regexp.MustCompile(`(?s)enum (\w+) \{([^}]*)\}`)
|
||||
reEnumMember = regexp.MustCompile(`(?m)^\s+(\w+),?\s*$`)
|
||||
reField = regexp.MustCompile(`(?m)^\s+([\w?]+):(.+)$`)
|
||||
)
|
||||
|
||||
// parseTsDir 解析 GenTs 输出目录(ts/ 子目录),重建 FunMeta。
|
||||
// 两遍解析:先收集全部 enum/interface 原始声明,再解析字段引用,
|
||||
// 避免文件顺序(字母序)导致前置类型解析不到
|
||||
func parseTsDir(tsDir string) (*FunMeta, error) {
|
||||
if _, err := os.Stat(filepath.Join(tsDir, "client.ts")); err != nil {
|
||||
return nil, fmt.Errorf("%s 不是 fun 生成的 ts 目录(缺 client.ts)", tsDir)
|
||||
}
|
||||
|
||||
enums := map[string][]string{}
|
||||
type rawField struct {
|
||||
Name string
|
||||
TsType string
|
||||
Optional bool
|
||||
}
|
||||
ifaceRaw := map[string][]rawField{}
|
||||
ifaceNode := map[string]*MetaType{}
|
||||
var serviceFiles []string
|
||||
|
||||
entries, err := os.ReadDir(tsDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".ts") {
|
||||
continue
|
||||
}
|
||||
if e.Name() == "client.ts" || e.Name() == "fun.ts" {
|
||||
continue
|
||||
}
|
||||
body, err := os.ReadFile(filepath.Join(tsDir, e.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
src := string(body)
|
||||
if reServiceClass.MatchString(src) {
|
||||
serviceFiles = append(serviceFiles, src)
|
||||
continue
|
||||
}
|
||||
if m := reEnum.FindStringSubmatch(src); m != nil {
|
||||
var names []string
|
||||
for _, line := range reEnumMember.FindAllStringSubmatch(m[2], -1) {
|
||||
names = append(names, line[1])
|
||||
}
|
||||
enums[m[1]] = names
|
||||
continue
|
||||
}
|
||||
if m := reInterface.FindStringSubmatch(src); m != nil {
|
||||
var fields []rawField
|
||||
for _, f := range reField.FindAllStringSubmatch(m[2], -1) {
|
||||
name, tsType := f[1], strings.TrimSpace(f[2])
|
||||
fields = append(fields, rawField{
|
||||
Name: strings.TrimSuffix(name, "?"),
|
||||
TsType: tsType,
|
||||
Optional: strings.HasSuffix(name, "?"),
|
||||
})
|
||||
}
|
||||
ifaceRaw[m[1]] = fields
|
||||
ifaceNode[m[1]] = &MetaType{Kind: "struct", Name: m[1]}
|
||||
}
|
||||
}
|
||||
|
||||
// 第二遍:填充 interface 字段(引用此时全部可见)
|
||||
resolve := func(tsType string) *MetaType { return parseTsType(tsType, ifaceNode, enums) }
|
||||
for name, fields := range ifaceRaw {
|
||||
node := ifaceNode[name]
|
||||
for _, f := range fields {
|
||||
ft := resolve(f.TsType)
|
||||
if f.Optional {
|
||||
ft.Optional = true
|
||||
}
|
||||
node.Fields = append(node.Fields, MetaField{Name: f.Name, Type: ft})
|
||||
}
|
||||
}
|
||||
|
||||
meta := &FunMeta{}
|
||||
for _, src := range serviceFiles {
|
||||
svcName := reServiceClass.FindStringSubmatch(src)[1]
|
||||
svc := MetaService{Name: svcName}
|
||||
for _, m := range reDataMethod.FindAllStringSubmatch(src, -1) {
|
||||
mm := MetaMethod{Name: m[1], Returns: resolve(m[3])}
|
||||
if m[3] == "void" {
|
||||
mm.Void = true
|
||||
mm.Returns = nil
|
||||
}
|
||||
if m[2] != "" {
|
||||
mm.DTO = resolve(m[2])
|
||||
}
|
||||
svc.Methods = append(svc.Methods, mm)
|
||||
}
|
||||
for _, m := range reStreamMethod.FindAllStringSubmatch(src, -1) {
|
||||
mm := MetaMethod{Name: m[1], IsStream: true}
|
||||
if m[2] != "" {
|
||||
mm.DTO = resolve(m[2])
|
||||
}
|
||||
if m[3] != "any" {
|
||||
mm.Returns = resolve(m[3])
|
||||
}
|
||||
svc.Methods = append(svc.Methods, mm)
|
||||
}
|
||||
sortMetaService(&svc)
|
||||
meta.Services = append(meta.Services, svc)
|
||||
}
|
||||
sortMeta(meta)
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
// parseTsType TS 类型串 → 元数据类型树
|
||||
func parseTsType(s string, interfaces map[string]*MetaType, enums map[string][]string) *MetaType {
|
||||
s = strings.TrimSpace(s)
|
||||
// 流式 dto 工厂类型 "X | (() => X)" 取值类型部分
|
||||
if i := strings.Index(s, " | ("); i >= 0 {
|
||||
s = s[:i]
|
||||
}
|
||||
// fun v1.3.7 流式模板对 DTO 会多写一层 "dto:" 前缀,兼容剥离
|
||||
s = strings.TrimPrefix(s, "dto:")
|
||||
t := &MetaType{}
|
||||
if strings.HasSuffix(s, " | null") {
|
||||
t.Optional = true
|
||||
s = strings.TrimSuffix(s, " | null")
|
||||
}
|
||||
if strings.HasSuffix(s, "[]") {
|
||||
t.Kind = "slice"
|
||||
t.Elem = parseTsType(strings.TrimSuffix(s, "[]"), interfaces, enums)
|
||||
return t
|
||||
}
|
||||
switch s {
|
||||
case "number":
|
||||
t.Kind, t.Name = "int", "int64"
|
||||
case "boolean":
|
||||
t.Kind, t.Name = "bool", "bool"
|
||||
case "string":
|
||||
t.Kind, t.Name = "string", "string"
|
||||
case "any":
|
||||
t.Kind, t.Name = "string", "any"
|
||||
default:
|
||||
if names, ok := enums[s]; ok {
|
||||
t.Kind, t.Name = "enum", s
|
||||
t.Names = names
|
||||
return t
|
||||
}
|
||||
if it, ok := interfaces[s]; ok {
|
||||
return it
|
||||
}
|
||||
t.Kind, t.Name = "struct", s // 定义在其他文件(如跨包),仅名字引用
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func sortMetaService(svc *MetaService) {
|
||||
for i := 1; i < len(svc.Methods); i++ {
|
||||
for j := i; j > 0 && svc.Methods[j].Name < svc.Methods[j-1].Name; j-- {
|
||||
svc.Methods[j], svc.Methods[j-1] = svc.Methods[j-1], svc.Methods[j]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sortMeta(meta *FunMeta) {
|
||||
for i := 1; i < len(meta.Services); i++ {
|
||||
for j := i; j > 0 && meta.Services[j].Name < meta.Services[j-1].Name; j-- {
|
||||
meta.Services[j], meta.Services[j-1] = meta.Services[j-1], meta.Services[j]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user