From 7d5c6b06f70812ef04028a39167478f066a4ec52 Mon Sep 17 00:00:00 2001 From: chiyi Date: Sat, 19 Sep 2026 16:14:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20fun=20new=20=E9=A1=B9=E7=9B=AE=E8=84=9A?= =?UTF-8?q?=E6=89=8B=E6=9E=B6=E2=80=94=E2=80=94=E4=B8=80=E6=9D=A1=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E7=94=9F=E6=88=90=E5=8F=AF=E8=BF=90=E8=A1=8C=20hello?= =?UTF-8?q?=20world?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fun new <名字|模块路径>:go.mod + HelloSvc.Say 示例 + README + .gitignore - -local 写 replace 指向本地 fun 源码(离线/未发布版本也能跑) - -port 指定示例端口;flag 手动解析可放任意位置 - 自动 go mod tidy,失败给出手动提示 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- README.md | 14 ++++- cmd/fun/main.go | 7 +++ cmd/fun/new.go | 154 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 cmd/fun/new.go diff --git a/README.md b/README.md index 6d3123a..539ac37 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,19 @@ fun 的服务注册全在 `BindService`/`BindServiceForGen` 调用里。fun-cli (服务定义在 `package main` 也不怕)——执行后立即删除。生成由 fun 公开的 `BindServiceForGen + SetOutput + GenCode` 完成,产物与内置生成器完全一致。 -## 三个命令 +## 四个命令 + +### fun new —— 项目脚手架 + +```bash +fun new myapp # 生成 go.mod + hello world 服务 +fun new github.com/me/myapp # 模块路径形式,目录取末段 +fun new myapp -local ../fun # replace 指向本地 fun 源码(离线/未发布) +fun new myapp -port 9000 # 指定示例服务端口(默认 18080) +``` + +生成 `go.mod`、`main.go`(HelloSvc.Say 示例)、`README.md`、`.gitignore`, +自动 `go mod tidy` 后即可 `go run .` 启动。 ### fun gen —— 生成客户端代码 diff --git a/cmd/fun/main.go b/cmd/fun/main.go index 485e6ab..b6af234 100644 --- a/cmd/fun/main.go +++ b/cmd/fun/main.go @@ -2,6 +2,7 @@ package main // fun-cli:fun 框架的配套命令行工具 // +// fun new 脚手架:生成可运行的 fun 项目骨架 // fun gen [ts|go] 生成 TypeScript / Go 客户端 // fun run 热更新开发循环 // fun docs 拉起 API 文档站(参数表单 + 在线发送测试) @@ -16,6 +17,9 @@ const version = "1.4.1" const usage = `fun-cli ` + version + ` — fun 框架配套工具 用法: + fun new <项目名|模块路径> [-port 端口] + 脚手架:生成 go.mod + 示例服务 + README,go mod tidy 后即可 go run。 + fun gen [ts|go] [-o 目录] [-p 模块路径] [-from meta.json] 生成客户端代码(fun 核心直接产出,与内置 GenCode 完全一致)。 扫描模块内 BindService/BindServiceForGen 调用,在对应包目录临时 @@ -30,6 +34,7 @@ const usage = `fun-cli ` + version + ` — fun 框架配套工具 流式方法实时展示 NDJSON 行。默认自动 go run 拉起后端并反向代理 /cell。 示例: + fun new myapp fun gen ts -o frontend/src/api fun run -p ./cmd/server fun docs -upstream http://127.0.0.1:9000 -p . @@ -44,6 +49,8 @@ func main() { os.Exit(2) } switch os.Args[1] { + case "new": + cmdNew(os.Args[2:]) case "gen": cmdGen(os.Args[2:]) case "run": diff --git a/cmd/fun/new.go b/cmd/fun/new.go new file mode 100644 index 0000000..1700fe6 --- /dev/null +++ b/cmd/fun/new.go @@ -0,0 +1,154 @@ +package main + +// fun new:脚手架——一条命令生成可运行的 fun 项目骨架 + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" +) + +var reModuleName = regexp.MustCompile(`^[a-zA-Z0-9][\w.\-/]*$`) + +func cmdNew(args []string) { + // 手动解析:允许 flag 放任意位置(go flag 遇位置参数即停,不友好) + port, local, module := 18080, "", "" + for i := 0; i < len(args); i++ { + a := args[i] + if v, ok := strings.CutPrefix(a, "-port="); ok { + fmt.Sscanf(v, "%d", &port) + continue + } + if v, ok := strings.CutPrefix(a, "-local="); ok { + local = v + continue + } + if (a == "-port" || a == "-local") && i+1 < len(args) { + i++ + if a == "-port" { + fmt.Sscanf(args[i], "%d", &port) + } else { + local = args[i] + } + continue + } + if strings.HasPrefix(a, "-") { + fmt.Fprintln(os.Stderr, "fun: 未知参数:", a) + os.Exit(2) + } + if module != "" { + fmt.Fprintln(os.Stderr, "用法: fun new <项目名|模块路径> [-port 端口] [-local fun源码路径]") + os.Exit(2) + } + module = a + } + if module == "" { + fmt.Fprintln(os.Stderr, "用法: fun new <项目名|模块路径> [-port 端口] [-local fun源码路径]") + fmt.Fprintln(os.Stderr, "示例: fun new myapp fun new github.com/me/myapp") + os.Exit(2) + } + if !reModuleName.MatchString(module) || strings.Contains(module, "..") { + fmt.Fprintln(os.Stderr, "fun: 非法项目名:", module) + os.Exit(1) + } + // 模块路径形如 github.com/me/app 时,目录取最后一段 + dir := module + if i := strings.LastIndex(module, "/"); i >= 0 { + dir = module[i+1:] + } + if _, err := os.Stat(dir); err == nil { + fmt.Fprintf(os.Stderr, "fun: 目录 %s 已存在\n", dir) + os.Exit(1) + } + if err := os.MkdirAll(dir, 0o755); err != nil { + fmt.Fprintln(os.Stderr, "fun: 创建目录失败:", err) + os.Exit(1) + } + + gomod := fmt.Sprintf("module %s\n\ngo 1.22\n\nrequire github.com/cyi-cc/fun v1.4.1\n", module) + if local != "" { + abs, err := filepath.Abs(local) + if err != nil { + abs = local + } + gomod += fmt.Sprintf("\nreplace github.com/cyi-cc/fun => %s\n", filepath.ToSlash(abs)) + } + files := map[string]string{ + "go.mod": gomod, + "main.go": fmt.Sprintf(`package main + +import "github.com/cyi-cc/fun" + +type HelloDto struct { + Name string +} + +type HelloSvc struct { + fun.Ctx +} + +// hello world:最小可调用的服务方法 +func (s *HelloSvc) Say(dto HelloDto) (string, error) { + return "hello, " + dto.Name + "!", nil +} + +func main() { + f := fun.New() + f.BindService(&HelloSvc{}) + f.Start(%d) +} +`, port), + ".gitignore": "/gen\n/log\nfun-docs-edits.json\n", + "README.md": fmt.Sprintf(`# %s + +fun 框架项目。[文档](https://fungo.ink) + +## 启动 + +`+"```bash"+` +go run . +`+"```"+` + +## 工具链(fun-cli) + +`+"```bash"+` +fun docs # API 文档站 + 在线调测(热更新) +fun run # 热更新开发循环 +fun gen ts # 生成 TypeScript 客户端 +`+"```"+` +`, module), + } + for name, body := range files { + if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil { + fmt.Fprintf(os.Stderr, "fun: 写入 %s 失败: %v\n", name, err) + os.Exit(1) + } + } + + tidy := exec.Command("go", "mod", "tidy") + tidy.Dir = dir + tidy.Stdout, tidy.Stderr = os.Stderr, os.Stderr + tidied := tidy.Run() == nil + + next := "go run ." + if !tidied { + next = "go mod tidy && go run ." + } + fmt.Printf(` + 已创建 %s(module %s) + ───────────────────────────────────────── + %s/go.mod 依赖 github.com/cyi-cc/fun + %s/main.go HelloSvc.Say 示例服务 + ───────────────────────────────────────── + 下一步: + cd %s + %s # 启动服务(:%d) + fun docs # 文档站在线调测 +`, dir, module, dir, dir, dir, next, port) + if !tidied { + fmt.Println("\n ⚠ go mod tidy 失败(可能离线),进入目录后手动执行一次即可") + } +}