Files
fun-cli/cmd/fun/tsparse.go
T

187 lines
5.3 KiB
Go

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]
}
}
}