From e9877b519cec2e504a7006fa38c73239203c74fd Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Tue, 4 Mar 2025 11:24:09 +0530 Subject: [PATCH 1/4] add module flag --- console/commands/new_command.go | 119 +++++++++++++++++++++++---- console/commands/new_command_test.go | 23 +++--- support/constant.go | 2 + 3 files changed, 119 insertions(+), 25 deletions(-) diff --git a/console/commands/new_command.go b/console/commands/new_command.go index e256236..d921498 100644 --- a/console/commands/new_command.go +++ b/console/commands/new_command.go @@ -1,6 +1,7 @@ package commands import ( + "bufio" "errors" "fmt" "io" @@ -8,6 +9,7 @@ import ( "os/exec" "path/filepath" "regexp" + "strings" "github.com/goravel/framework/contracts/console" "github.com/goravel/framework/contracts/console/command" @@ -22,17 +24,17 @@ type NewCommand struct { } // 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{ @@ -40,12 +42,17 @@ func (receiver *NewCommand) Extend() command.Extend { 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) @@ -72,30 +79,54 @@ func (receiver *NewCommand) Handle(ctx console.Context) (err error) { } 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) @@ -121,7 +152,7 @@ func (receiver *NewCommand) generate(ctx console.Context, name string) error { // 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 { @@ -130,6 +161,20 @@ func (receiver *NewCommand) generate(ctx console.Context, name string) error { } 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 @@ -149,7 +194,7 @@ func (receiver *NewCommand) generate(ctx console.Context, name string) error { 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 { @@ -177,7 +222,53 @@ func (receiver *NewCommand) generate(ctx console.Context, name string) error { 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 + + 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 @@ -187,7 +278,7 @@ func (receiver *NewCommand) removeFiles(path string) error { 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 { diff --git a/console/commands/new_command_test.go b/console/commands/new_command_test.go index 552a2d4..6aff61d 100644 --- a/console/commands/new_command_test.go +++ b/console/commands/new_command_test.go @@ -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)) }) @@ -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") diff --git a/support/constant.go b/support/constant.go index 8b058bc..1d9c7f6 100644 --- a/support/constant.go +++ b/support/constant.go @@ -8,3 +8,5 @@ const WelcomeHeading = ` | (_ || (_) || / / _ \\ V / | _| | |__ \___| \___/ |_|_\/_/ \_\\_/ |___||____| ` + +const DefaultModuleName = "goravel" From 5365de56ad07e44658a02b5666c3560c9dd70d36 Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Wed, 5 Mar 2025 15:33:27 +0530 Subject: [PATCH 2/4] write test cases --- console/commands/new_command.go | 6 ++-- console/commands/new_command_test.go | 48 ++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/console/commands/new_command.go b/console/commands/new_command.go index d921498..831ced5 100644 --- a/console/commands/new_command.go +++ b/console/commands/new_command.go @@ -87,16 +87,16 @@ func (r *NewCommand) Handle(ctx console.Context) (err error) { module := ctx.Option("module") if module == "" { module, err = ctx.Ask("What is the module name?", console.AskOption{ - Placeholder: "E.g github.com/yourusername/yourproject", + 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") + return errors.New("module name is required") } if !regexp.MustCompile(`^[a-zA-Z0-9./-]+$`).MatchString(value) { - return errors.New("invalid module name format") + return errors.New("invalid module name format. Use only letters, numbers, dots (.), slashes (/), and hyphens (-). Example: github.com/yourusername/yourproject") } return nil diff --git a/console/commands/new_command_test.go b/console/commands/new_command_test.go index 6aff61d..620a18e 100644 --- a/console/commands/new_command_test.go +++ b/console/commands/new_command_test.go @@ -7,13 +7,14 @@ import ( "path/filepath" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/goravel/framework/contracts/console" "github.com/goravel/framework/contracts/console/command" consolemocks "github.com/goravel/framework/mocks/console" "github.com/goravel/framework/support/color" "github.com/goravel/framework/support/file" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" ) func TestNewCommand(t *testing.T) { @@ -78,3 +79,46 @@ func TestCopyFile(t *testing.T) { assert.Nil(t, os.Remove(src)) assert.Nil(t, os.Remove(dst)) } + +func TestReplaceModule(t *testing.T) { + newCommand := &NewCommand{} + + tmpDir, err := os.MkdirTemp("", "test-replace-module") + assert.Nil(t, err) + defer os.RemoveAll(tmpDir) + + goFile := filepath.Join(tmpDir, "main.go") + modFile := filepath.Join(tmpDir, "go.mod") + invalidFile := filepath.Join(tmpDir, "ignore.txt") + + newModule := "github.com/example/project" + + // Write content to files + err = os.WriteFile(goFile, []byte(`package main +import "goravel/utils" +func main() {}`), os.ModePerm) + assert.Nil(t, err) + + err = os.WriteFile(modFile, []byte("module goravel\nrequire example.com v1.0.0"), os.ModePerm) + assert.Nil(t, err) + + err = os.WriteFile(invalidFile, []byte("This is a test file."), os.ModePerm) + assert.Nil(t, err) + + err = newCommand.replaceModule(tmpDir, newModule) + assert.Nil(t, err) + + modContent, err := os.ReadFile(modFile) + assert.Nil(t, err) + assert.Contains(t, string(modContent), "module github.com/example/project") + assert.NotContains(t, string(modContent), "module goravel") + + goContent, err := os.ReadFile(goFile) + assert.Nil(t, err) + assert.Contains(t, string(goContent), "import \"github.com/example/project/utils\"") + assert.NotContains(t, string(goContent), "import \"goravel/utils\"") + + invalidContent, err := os.ReadFile(invalidFile) + assert.Nil(t, err) + assert.Equal(t, "This is a test file.", string(invalidContent)) +} From cac77b2257f5029ec60bdc8c4157d881e228baa2 Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Wed, 5 Mar 2025 15:37:46 +0530 Subject: [PATCH 3/4] update version --- console/commands/new_command.go | 2 +- support/constant.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/console/commands/new_command.go b/console/commands/new_command.go index 831ced5..d7bee96 100644 --- a/console/commands/new_command.go +++ b/console/commands/new_command.go @@ -243,7 +243,7 @@ func (r *NewCommand) replaceModule(path, module string) error { scanner := bufio.NewScanner(fileContent) for scanner.Scan() { line := scanner.Text() - newLine := line + var newLine string if strings.HasSuffix(filePath, ".mod") { newLine = reModule.ReplaceAllString(line, "module "+module) diff --git a/support/constant.go b/support/constant.go index 1d9c7f6..bf8b587 100644 --- a/support/constant.go +++ b/support/constant.go @@ -1,6 +1,6 @@ package support -const Version string = "v1.1.1" +const Version string = "v1.2.0" const WelcomeHeading = ` ___ ___ ___ _ __ __ ___ _ From 7712ce67fbd1b0a7115d96766ae29705f3e7ff89 Mon Sep 17 00:00:00 2001 From: kkumar-gcc Date: Wed, 5 Mar 2025 17:07:29 +0530 Subject: [PATCH 4/4] resolve comments --- console/commands/new_command.go | 4 ++-- support/constant.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/console/commands/new_command.go b/console/commands/new_command.go index d7bee96..a0a9f28 100644 --- a/console/commands/new_command.go +++ b/console/commands/new_command.go @@ -95,8 +95,8 @@ func (r *NewCommand) Handle(ctx console.Context) (err error) { return errors.New("module name is required") } - if !regexp.MustCompile(`^[a-zA-Z0-9./-]+$`).MatchString(value) { - return errors.New("invalid module name format. Use only letters, numbers, dots (.), slashes (/), and hyphens (-). Example: github.com/yourusername/yourproject") + if !regexp.MustCompile(`^[a-zA-Z0-9./_-]+$`).MatchString(value) { + return errors.New("invalid module name format. Use only letters, numbers, dots (.), slashes (/), underscores (_), and hyphens (-). Example: [github.com/yourusername/yourproject] or [yourproject]") } return nil diff --git a/support/constant.go b/support/constant.go index bf8b587..a3a8d38 100644 --- a/support/constant.go +++ b/support/constant.go @@ -1,6 +1,6 @@ package support -const Version string = "v1.2.0" +const Version string = "v1.1.2" const WelcomeHeading = ` ___ ___ ___ _ __ __ ___ _