Skip to content

Commit 4a53f1e

Browse files
committed
feat: initial project
0 parents  commit 4a53f1e

File tree

10 files changed

+648
-0
lines changed

10 files changed

+648
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.idea

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright © 2025 George <george@betterde.com>
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Installation
2+
3+
```shell
4+
go install github.com/betterde/gonew@latest
5+
```
6+
7+
# Usage
8+
9+
```shell
10+
gonew init <SOURCE_MODULE> [DEST_MODULE]
11+
```
12+
13+
# Custom project template
14+
15+
Please refer to the repository `github.com/betterde/template/fiber`

cmd/init.go

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
/*
2+
Copyright © 2025 George <george@betterde.com>
3+
4+
Permission is hereby granted, free of charge, to any person obtaining a copy
5+
of this software and associated documentation files (the "Software"), to deal
6+
in the Software without restriction, including without limitation the rights
7+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
copies of the Software, and to permit persons to whom the Software is
9+
furnished to do so, subject to the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included in
12+
all copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20+
THE SOFTWARE.
21+
*/
22+
package cmd
23+
24+
import (
25+
"bytes"
26+
"encoding/json"
27+
"fmt"
28+
"github.com/betterde/gonew/internal/edit"
29+
"github.com/manifoldco/promptui"
30+
"github.com/spf13/cobra"
31+
"go/parser"
32+
"go/token"
33+
"golang.org/x/mod/modfile"
34+
"golang.org/x/mod/module"
35+
"gopkg.in/yaml.v3"
36+
"io/fs"
37+
"log"
38+
"os"
39+
"os/exec"
40+
"path"
41+
"path/filepath"
42+
"strconv"
43+
"strings"
44+
"text/template"
45+
)
46+
47+
// initCmd represents the init command
48+
var initCmd = &cobra.Command{
49+
Use: "init <src> [dst]",
50+
Run: initProject,
51+
Args: cobra.MinimumNArgs(1),
52+
Short: "Initialize a new project using a template",
53+
}
54+
55+
var (
56+
src string
57+
dst string
58+
)
59+
60+
func init() {
61+
rootCmd.AddCommand(initCmd)
62+
63+
// Here you will define your flags and configuration settings.
64+
65+
// Cobra supports Persistent Flags which will work for this command
66+
// and all subcommands, e.g.:
67+
// initCmd.PersistentFlags().String("foo", "", "A help for foo")
68+
69+
// Cobra supports local flags which will only run when this command
70+
// is called directly, e.g.:
71+
// initCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
72+
}
73+
74+
func initProject(cmd *cobra.Command, args []string) {
75+
if len(args) < 1 || len(args) > 3 {
76+
err := cmd.Usage()
77+
if err != nil {
78+
return
79+
}
80+
}
81+
82+
src = args[0]
83+
ver := src
84+
if !strings.Contains(ver, "@") {
85+
ver += "@latest"
86+
}
87+
88+
src, _, _ = strings.Cut(src, "@")
89+
if err := module.CheckPath(src); err != nil {
90+
log.Fatalf("invalid source module name: %v", err)
91+
}
92+
93+
dst = src
94+
if len(args) >= 2 {
95+
dst = args[1]
96+
if err := module.CheckPath(dst); err != nil {
97+
log.Fatalf("invalid destination module name: %v", err)
98+
}
99+
}
100+
101+
var dir string
102+
if len(args) == 3 {
103+
dir = args[2]
104+
} else {
105+
dir = "." + string(filepath.Separator) + path.Base(dst)
106+
}
107+
108+
// Dir must not exist or must be an empty directory.
109+
de, err := os.ReadDir(dir)
110+
if err == nil && len(de) > 0 {
111+
log.Fatalf("target directory %s exists and is non-empty", dir)
112+
}
113+
needMkdir := err != nil
114+
115+
var stdout, stderr bytes.Buffer
116+
command := exec.Command("go", "mod", "download", "-json", ver)
117+
command.Stdout = &stdout
118+
command.Stderr = &stderr
119+
if err = command.Run(); err != nil {
120+
log.Fatalf("go mod download -json %s: %v\n%s%s", ver, err, stderr.Bytes(), stdout.Bytes())
121+
}
122+
123+
var info struct {
124+
Dir string
125+
}
126+
if err = json.Unmarshal(stdout.Bytes(), &info); err != nil {
127+
log.Fatalf("go mod download -json %s: invalid JSON output: %v\n%s%s", ver, err, stderr.Bytes(), stdout.Bytes())
128+
}
129+
130+
if needMkdir {
131+
if err := os.MkdirAll(dir, 0777); err != nil {
132+
log.Fatalf("mkdir error: %s", err)
133+
}
134+
}
135+
136+
// Copy from module cache into new directory, making edits as needed.
137+
err = filepath.WalkDir(info.Dir, func(src string, d fs.DirEntry, err error) error {
138+
if err != nil {
139+
log.Fatal(err)
140+
}
141+
rel, err := filepath.Rel(info.Dir, src)
142+
if err != nil {
143+
log.Fatal(err)
144+
}
145+
dstPath := filepath.Join(dir, rel)
146+
if d.IsDir() {
147+
if err := os.MkdirAll(dstPath, 0777); err != nil {
148+
log.Fatal(err)
149+
}
150+
return nil
151+
}
152+
153+
data, err := os.ReadFile(src)
154+
if err != nil {
155+
log.Fatal(err)
156+
}
157+
158+
isRoot := !strings.Contains(rel, string(filepath.Separator))
159+
if strings.HasSuffix(rel, ".go") {
160+
data = fixGo(data, rel, src, dst, isRoot)
161+
}
162+
if rel == "go.mod" {
163+
data = fixGoMod(data, dst)
164+
}
165+
166+
if err := os.WriteFile(dstPath, data, 0666); err != nil {
167+
log.Fatal(err)
168+
}
169+
return nil
170+
})
171+
if err != nil {
172+
log.Fatal(err)
173+
}
174+
175+
templateFile := filepath.Join(dir, "template.yaml")
176+
prompts, err := readConfig(templateFile)
177+
if err != nil {
178+
log.Fatal(err)
179+
}
180+
181+
inputs, err := runPrompts(prompts)
182+
if err != nil {
183+
log.Fatal(err)
184+
}
185+
186+
err = replaceVars(dir, inputs)
187+
if err != nil {
188+
log.Fatal(err)
189+
}
190+
191+
log.Printf("initialized %s in %s", dst, dir)
192+
}
193+
194+
// fixGo rewrites the Go source in data to replace srcMod with dstMod.
195+
// isRoot indicates whether the file is in the root directory of the module,
196+
// in which case we also update the package name.
197+
func fixGo(data []byte, file string, srcMod, dstMod string, isRoot bool) []byte {
198+
fileSet := token.NewFileSet()
199+
f, err := parser.ParseFile(fileSet, file, data, parser.ImportsOnly)
200+
if err != nil {
201+
log.Fatalf("parsing source module:\n%s", err)
202+
}
203+
204+
buf := edit.NewBuffer(data)
205+
at := func(p token.Pos) int {
206+
return fileSet.File(p).Offset(p)
207+
}
208+
209+
srcName := path.Base(srcMod)
210+
dstName := path.Base(dstMod)
211+
if isRoot {
212+
if name := f.Name.Name; name == srcName || name == srcName+"_test" {
213+
dname := dstName + strings.TrimPrefix(name, srcName)
214+
if !token.IsIdentifier(dname) {
215+
log.Fatalf("%s: cannot rename package %s to package %s: invalid package name", file, name, dname)
216+
}
217+
buf.Replace(at(f.Name.Pos()), at(f.Name.End()), dname)
218+
}
219+
}
220+
221+
for _, spec := range f.Imports {
222+
path, err := strconv.Unquote(spec.Path.Value)
223+
if err != nil {
224+
continue
225+
}
226+
if path == srcMod {
227+
if srcName != dstName && spec.Name == nil {
228+
// Add package rename because source code uses original name.
229+
// The renaming looks strange, but template authors are unlikely to
230+
// create a template where the root package is imported by packages
231+
// in subdirectories, and the renaming at least keeps the code working.
232+
// A more sophisticated approach would be to rename the uses of
233+
// the package identifier in the file too, but then you have to worry about
234+
// name collisions, and given how unlikely this is, it doesn't seem worth
235+
// trying to clean up the file that way.
236+
buf.Insert(at(spec.Path.Pos()), srcName+" ")
237+
}
238+
// Change import path to dstMod
239+
buf.Replace(at(spec.Path.Pos()), at(spec.Path.End()), strconv.Quote(dstMod))
240+
}
241+
if strings.HasPrefix(path, srcMod+"/") {
242+
// Change import path to begin with dstMod
243+
buf.Replace(at(spec.Path.Pos()), at(spec.Path.End()), strconv.Quote(strings.Replace(path, srcMod, dstMod, 1)))
244+
}
245+
}
246+
return buf.Bytes()
247+
}
248+
249+
// fixGoMod rewrites the go.mod content in data to replace srcMod with dstMod
250+
// in the module path.
251+
func fixGoMod(data []byte, dstMod string) []byte {
252+
file, err := modfile.ParseLax("go.mod", data, nil)
253+
if err != nil {
254+
log.Fatalf("parsing source module:\n%s", err)
255+
}
256+
err = file.AddModuleStmt(dstMod)
257+
if err != nil {
258+
log.Fatalf("add module stmt:\n%s", err)
259+
}
260+
format, err := file.Format()
261+
if err != nil {
262+
return data
263+
}
264+
return format
265+
}
266+
267+
// readConfig Reading YAML configuration files
268+
func readConfig(filename string) (map[string]string, error) {
269+
var config map[string]string
270+
data, err := os.ReadFile(filename)
271+
if err != nil {
272+
return config, err
273+
}
274+
err = yaml.Unmarshal(data, &config)
275+
return config, err
276+
}
277+
278+
// runPrompts Run interactive prompts based on configuration
279+
func runPrompts(config map[string]string) (map[string]string, error) {
280+
answers := make(map[string]string)
281+
282+
for key, desc := range config {
283+
prompt := promptui.Prompt{
284+
Label: desc,
285+
Validate: func(input string) error {
286+
if len(input) == 0 {
287+
return fmt.Errorf(desc)
288+
}
289+
return nil
290+
},
291+
}
292+
293+
name, err := prompt.Run()
294+
if err != nil {
295+
return nil, err
296+
}
297+
answers[key] = name
298+
}
299+
300+
return answers, nil
301+
}
302+
303+
func replaceVars(dir string, inputs map[string]string) error {
304+
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
305+
if err != nil {
306+
return err
307+
}
308+
if !info.IsDir() {
309+
relPath, err := filepath.Rel(dir, path)
310+
if err != nil {
311+
return err
312+
}
313+
content, err := os.ReadFile(path)
314+
if err != nil {
315+
return err
316+
}
317+
318+
return generateFile(inputs, relPath, string(content), dir)
319+
}
320+
return nil
321+
})
322+
}
323+
324+
// generateFile creates a single file from a template
325+
func generateFile(data map[string]string, fileName, content, projectDir string) error {
326+
// Parse the template
327+
tmpl, err := template.New(fileName).Parse(content)
328+
if err != nil {
329+
return fmt.Errorf("error parsing template %s: %v", fileName, err)
330+
}
331+
332+
// Create the output file
333+
filePath := filepath.Join(projectDir, fileName)
334+
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
335+
return fmt.Errorf("error creating directories for %s: %v", fileName, err)
336+
}
337+
338+
file, err := os.Create(filePath)
339+
if err != nil {
340+
return fmt.Errorf("error creating file %s: %v", fileName, err)
341+
}
342+
defer file.Close()
343+
344+
// Execute the template and write to file
345+
if err := tmpl.Execute(file, data); err != nil {
346+
return fmt.Errorf("error executing template %s: %v", fileName, err)
347+
}
348+
349+
return nil
350+
}

0 commit comments

Comments
 (0)