Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add custom module name #27

Merged
merged 6 commits into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 105 additions & 14 deletions console/commands/new_command.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package commands

import (
"bufio"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"

"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/contracts/console/command"
Expand All @@ -22,30 +24,35 @@
}

// Signature The name and signature of the console command.
func (receiver *NewCommand) Signature() string {
func (r *NewCommand) Signature() string {
return "new"
}

// Description The console command description.
func (receiver *NewCommand) Description() string {
func (r *NewCommand) Description() string {
return "Create a new Goravel application"
}

// Extend The console command extend.
func (receiver *NewCommand) Extend() command.Extend {
func (r *NewCommand) Extend() command.Extend {
return command.Extend{
Flags: []command.Flag{
&command.BoolFlag{
Name: "force",
Aliases: []string{"f"},
Usage: "Forces install even if the directory already exists",
},
&command.StringFlag{
Name: "module",
Aliases: []string{"m"},
Usage: "Specify the custom module name to replace the default 'goravel' module",
},
},
}
}

// Handle Execute the console command.
func (receiver *NewCommand) Handle(ctx console.Context) (err error) {
func (r *NewCommand) Handle(ctx console.Context) (err error) {
fmt.Println(pterm.NewRGB(52, 124, 153).Sprint(support.WelcomeHeading)) // color hex code: #8ED3F9
ctx.NewLine()
name := ctx.Argument(0)
Expand All @@ -72,30 +79,54 @@
}

force := ctx.OptionBool("force")
if !force && receiver.verifyIfDirectoryExists(receiver.getPath(name)) {
if !force && r.verifyIfDirectoryExists(r.getPath(name)) {
color.Errorln("the directory already exists. use the --force flag to overwrite")
return nil
}

return receiver.generate(ctx, name)
module := ctx.Option("module")
if module == "" {
module, err = ctx.Ask("What is the module name?", console.AskOption{
Placeholder: "E.g github.com/yourusername/yourproject",
Default: support.DefaultModuleName,
Prompt: ">",
Validate: func(value string) error {
if value == "" {
return errors.New("the module name is required")
}

if !regexp.MustCompile(`^[a-zA-Z0-9./-]+$`).MatchString(value) {
return errors.New("invalid module name format")
}

return nil
},
})
if err != nil {
color.Errorln(err.Error())
return nil
}
}

return r.generate(ctx, name, module)
}

// verifyIfDirectoryExists Verify if the directory already exists.
func (receiver *NewCommand) verifyIfDirectoryExists(path string) bool {
func (r *NewCommand) verifyIfDirectoryExists(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
}

// getPath Get the full path to the command.
func (receiver *NewCommand) getPath(name string) string {
func (r *NewCommand) getPath(name string) string {
pwd, _ := os.Getwd()

return filepath.Clean(filepath.Join(pwd, name))
}

// generate Generate the project.
func (receiver *NewCommand) generate(ctx console.Context, name string) error {
path := receiver.getPath(name)
func (r *NewCommand) generate(ctx console.Context, name string, module string) error {
path := r.getPath(name)
name = filepath.Clean(name)
bold := pterm.NewStyle(pterm.Bold)

Expand All @@ -121,7 +152,7 @@
// git cleanup
err = ctx.Spinner("> @rm -rf "+name+"/.git "+name+"/.github", console.SpinnerOption{
Action: func() error {
return receiver.removeFiles(path)
return r.removeFiles(path)
},
})
if err != nil {
Expand All @@ -130,6 +161,20 @@
}
color.Successln("git cleanup done")

// Replace the module name if it's different from the default
if module != support.DefaultModuleName {
err = ctx.Spinner("Updating module name to \""+module+"\"", console.SpinnerOption{
Action: func() error {
return r.replaceModule(path, module)
},
})
if err != nil {
color.Errorf("Failed to update module name: %s\n", err)
return nil
}
color.Successln("Module name updated successfully!")
}

// install dependencies
install := exec.Command("go", "mod", "tidy")
install.Dir = path
Expand All @@ -149,7 +194,7 @@
Action: func() error {
inputFilePath := filepath.Join(path, ".env.example")
outputFilePath := filepath.Join(path, ".env")
return receiver.copyFile(inputFilePath, outputFilePath)
return r.copyFile(inputFilePath, outputFilePath)
},
})
if err != nil {
Expand Down Expand Up @@ -177,7 +222,53 @@
return nil
}

func (receiver *NewCommand) removeFiles(path string) error {
func (r *NewCommand) replaceModule(path, module string) error {
module = strings.Trim(module, "/")
reModule := regexp.MustCompile(`^module\s+goravel\b`)
reImport := regexp.MustCompile(`"goravel/([^"]+)"`)

return filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || (!strings.HasSuffix(filePath, ".go") && !strings.HasSuffix(filePath, ".mod")) {
return err
}

fileContent, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("error opening %s: %w", filePath, err)
}
defer fileContent.Close()

var newContent strings.Builder
var modified bool
scanner := bufio.NewScanner(fileContent)
for scanner.Scan() {
line := scanner.Text()
newLine := line

Check failure on line 246 in console/commands/new_command.go

View workflow job for this annotation

GitHub Actions / lint / lint

ineffectual assignment to newLine (ineffassign)

if strings.HasSuffix(filePath, ".mod") {
newLine = reModule.ReplaceAllString(line, "module "+module)
} else {
newLine = reImport.ReplaceAllString(line, `"`+module+`/$1"`)
}

if newLine != line {
modified = true
}
newContent.WriteString(newLine + "\n")
}

if err := scanner.Err(); err != nil {
return fmt.Errorf("error reading %s: %w", filePath, err)
}

if modified {
return os.WriteFile(filePath, []byte(newContent.String()), 0644)
}
return nil
})
}

func (r *NewCommand) removeFiles(path string) error {
// Remove the .git directory
if err := file.Remove(filepath.Join(path, ".git")); err != nil {
return err
Expand All @@ -187,7 +278,7 @@
return file.Remove(filepath.Join(path, ".github"))
}

func (receiver *NewCommand) copyFile(inputFilePath, outputFilePath string) (err error) {
func (r *NewCommand) copyFile(inputFilePath, outputFilePath string) (err error) {
// Open .env.example file
in, err := os.Open(inputFilePath)
if err != nil {
Expand Down
23 changes: 12 additions & 11 deletions console/commands/new_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,21 @@ func TestNewCommand(t *testing.T) {
})

mockContext := &consolemocks.Context{}
mockContext.On("Argument", 0).Return("").Once()
mockContext.On("Ask", "What is the name of your project?", mock.Anything).Return("", errors.New("the project name is required")).Once()
mockContext.On("NewLine").Return()
mockContext.On("Spinner", mock.Anything, mock.AnythingOfType("console.SpinnerOption")).Return(nil).
Run(func(args mock.Arguments) {
options := args.Get(1).(console.SpinnerOption)
mockContext.EXPECT().Argument(0).Return("").Once()
mockContext.EXPECT().Ask("What is the name of your project?", mock.Anything).Return("", errors.New("the project name is required")).Once()
mockContext.EXPECT().NewLine().Return()
mockContext.EXPECT().Spinner(mock.Anything, mock.AnythingOfType("console.SpinnerOption")).Return(nil).
Run(func(message string, options console.SpinnerOption) {
assert.Nil(t, options.Action())
}).Times(5)
}).Times(6)
assert.Contains(t, color.CaptureOutput(func(w io.Writer) {
assert.Nil(t, newCommand.Handle(mockContext))
}), "the project name is required")

mockContext.On("Argument", 0).Return("example-app").Once()
mockContext.On("OptionBool", "force").Return(true).Once()
mockContext.EXPECT().Argument(0).Return("example-app").Once()
mockContext.EXPECT().OptionBool("force").Return(true).Once()
mockContext.EXPECT().Option("module").Return("").Once()
mockContext.EXPECT().Ask("What is the module name?", mock.Anything).Return("github.com/example/", nil).Once()
captureOutput := color.CaptureOutput(func(w io.Writer) {
assert.Nil(t, newCommand.Handle(mockContext))
})
Expand All @@ -50,8 +51,8 @@ func TestNewCommand(t *testing.T) {
assert.True(t, file.Exists("example-app"))
assert.True(t, file.Exists(filepath.Join("example-app", ".env")))

mockContext.On("Argument", 0).Return("example-app").Once()
mockContext.On("OptionBool", "force").Return(false).Once()
mockContext.EXPECT().Argument(0).Return("example-app").Once()
mockContext.EXPECT().OptionBool("force").Return(false).Once()
assert.Contains(t, color.CaptureOutput(func(w io.Writer) {
assert.Nil(t, newCommand.Handle(mockContext))
}), "the directory already exists. use the --force flag to overwrite")
Expand Down
2 changes: 2 additions & 0 deletions support/constant.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ const WelcomeHeading = `
| (_ || (_) || / / _ \\ V / | _| | |__
\___| \___/ |_|_\/_/ \_\\_/ |___||____|
`

const DefaultModuleName = "goravel"
Loading