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:
@@ -0,0 +1,196 @@
|
||||
package main
|
||||
|
||||
// fun run:热更新开发循环——改代码自动重编译重启。
|
||||
// 纯标准库实现:轮询 mtime 检测变更(400ms),先编译成功再切换进程,
|
||||
// 编译失败保留旧进程继续跑
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func cmdRun(args []string) {
|
||||
pkg := "."
|
||||
var childArgs []string
|
||||
for i := 0; i < len(args); i++ {
|
||||
if args[i] == "-p" && i+1 < len(args) {
|
||||
pkg = args[i+1]
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if args[i] == "--" {
|
||||
childArgs = append(childArgs, args[i+1:]...)
|
||||
break
|
||||
}
|
||||
if strings.HasPrefix(args[i], "-p") && len(args[i]) > 2 {
|
||||
pkg = args[i][2:]
|
||||
continue
|
||||
}
|
||||
childArgs = append(childArgs, args[i])
|
||||
}
|
||||
|
||||
binPath := filepath.Join(os.TempDir(), fmt.Sprintf("fun-run-%d", os.Getpid()))
|
||||
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
var child *exec.Cmd
|
||||
var childDone chan struct{}
|
||||
restart := make(chan struct{}, 1)
|
||||
|
||||
// 初始编译
|
||||
if !build(pkg, binPath) {
|
||||
fmt.Println("fun: 初始编译失败,等待文件变更后重试")
|
||||
} else {
|
||||
child, childDone = spawn(binPath, childArgs)
|
||||
}
|
||||
|
||||
go watchLoop(pkg, restart)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-sigs:
|
||||
fmt.Println("\nfun: 退出")
|
||||
killChild(child, childDone)
|
||||
return
|
||||
case <-restart:
|
||||
fmt.Println("fun: 检测到变更,重新编译...")
|
||||
if !build(pkg, binPath) {
|
||||
fmt.Println("fun: 编译失败,旧进程继续运行")
|
||||
continue
|
||||
}
|
||||
killChild(child, childDone)
|
||||
child, childDone = spawn(binPath, childArgs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func build(pkg, binPath string) bool {
|
||||
cmd := goCommand("build", pkg, "-o", binPath)
|
||||
cmd.Stdout, cmd.Stderr = os.Stderr, os.Stderr
|
||||
return cmd.Run() == nil
|
||||
}
|
||||
|
||||
func spawn(binPath string, args []string) (*exec.Cmd, chan struct{}) {
|
||||
cmd := exec.Command(binPath, args...)
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
// 独立进程组:杀父进程时连带子进程(业务可能 fork 了 worker)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
done := make(chan struct{})
|
||||
if err := cmd.Start(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "fun: 启动失败:", err)
|
||||
close(done)
|
||||
return nil, done
|
||||
}
|
||||
go func() {
|
||||
if err := cmd.Wait(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "fun: 进程退出: %v\n", err)
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
return cmd, done
|
||||
}
|
||||
|
||||
func killChild(child *exec.Cmd, done chan struct{}) {
|
||||
if child == nil || child.Process == nil {
|
||||
return
|
||||
}
|
||||
syscall.Kill(-child.Process.Pid, syscall.SIGTERM)
|
||||
if done == nil {
|
||||
done = make(chan struct{})
|
||||
go func() { child.Wait(); close(done) }()
|
||||
}
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(3 * time.Second):
|
||||
syscall.Kill(-child.Process.Pid, syscall.SIGKILL)
|
||||
}
|
||||
}
|
||||
|
||||
var skipDirs = map[string]bool{
|
||||
".git": true, "vendor": true, "node_modules": true, ".fun": true,
|
||||
"gen": true, "testdata": true,
|
||||
}
|
||||
|
||||
// snapshot 收集监控文件的 (路径, mtime)
|
||||
func snapshot(root string) map[string]int64 {
|
||||
out := map[string]int64{}
|
||||
filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
name := d.Name()
|
||||
if d.IsDir() {
|
||||
if path != root && skipDirs[name] {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
ext := filepath.Ext(name)
|
||||
if ext == ".go" || name == "go.mod" || name == "go.sum" {
|
||||
if info, err := d.Info(); err == nil {
|
||||
out[path] = info.ModTime().UnixNano()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func diffSnapshot(a, b map[string]int64) bool {
|
||||
if len(a) != len(b) {
|
||||
return true
|
||||
}
|
||||
for k, v := range a {
|
||||
if b[k] != v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func watchLoop(root string, changed chan struct{}) {
|
||||
last := snapshot(root)
|
||||
var pending bool
|
||||
var pendingAt time.Time
|
||||
for {
|
||||
time.Sleep(400 * time.Millisecond)
|
||||
cur := snapshot(root)
|
||||
if !diffSnapshot(last, cur) {
|
||||
// 静默 300ms 后触发(编辑器分多次写盘时合并)
|
||||
if pending && time.Since(pendingAt) > 300*time.Millisecond {
|
||||
pending = false
|
||||
select {
|
||||
case changed <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
last = cur
|
||||
continue
|
||||
}
|
||||
last = cur
|
||||
if !pending {
|
||||
pending = true
|
||||
pendingAt = time.Now()
|
||||
} else {
|
||||
pendingAt = time.Now()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sortedKeys 调试用(保留给未来扩展)
|
||||
func sortedKeys(m map[string]int64) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
Reference in New Issue
Block a user