Skip to content

Commit 2946b9b

Browse files
committed
feat: [#576] add package manager functionality
1 parent f437993 commit 2946b9b

22 files changed

+2212
-12
lines changed

contracts/packages/manager.go

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package packages
2+
3+
import (
4+
"github.com/dave/dst"
5+
"github.com/dave/dst/dstutil"
6+
)
7+
8+
type FileModifier interface {
9+
Apply(dir string) error
10+
}
11+
12+
type Manager interface {
13+
Install(dir string) error
14+
Uninstall(dir string) error
15+
}
16+
17+
type GoNodeMatcher interface {
18+
MatchNode(node dst.Node) bool
19+
MatchCursor(cursor *dstutil.Cursor) bool
20+
}
21+
22+
type GoNodeModifier interface {
23+
Apply(node dst.Node) error
24+
}

foundation/application.go

+2
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ func (app *Application) Boot() {
6161
console.NewEnvDecryptCommand(),
6262
console.NewTestMakeCommand(),
6363
console.NewPackageMakeCommand(),
64+
console.NewPackageInstallCommand(),
65+
console.NewPackageUninstallCommand(),
6466
console.NewVendorPublishCommand(app.publishes, app.publishGroups),
6567
})
6668
app.setTimezone()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package console
2+
3+
import (
4+
"fmt"
5+
"os/exec"
6+
"strings"
7+
8+
"github.com/goravel/framework/contracts/console"
9+
"github.com/goravel/framework/contracts/console/command"
10+
"github.com/goravel/framework/errors"
11+
"github.com/goravel/framework/support/color"
12+
)
13+
14+
type PackageInstallCommand struct {
15+
}
16+
17+
func NewPackageInstallCommand() *PackageInstallCommand {
18+
return &PackageInstallCommand{}
19+
}
20+
21+
// Signature The name and signature of the console command.
22+
func (r *PackageInstallCommand) Signature() string {
23+
return "package:install"
24+
}
25+
26+
// Description The console command description.
27+
func (r *PackageInstallCommand) Description() string {
28+
return "Install a package"
29+
}
30+
31+
// Extend The console command extend.
32+
func (r *PackageInstallCommand) Extend() command.Extend {
33+
return command.Extend{
34+
ArgsUsage: " <package@version>",
35+
Category: "package",
36+
}
37+
}
38+
39+
// Handle Execute the console command.
40+
func (r *PackageInstallCommand) Handle(ctx console.Context) error {
41+
var (
42+
err error
43+
pkg = ctx.Argument(0)
44+
)
45+
if pkg == "" {
46+
pkg, err = ctx.Ask("Enter the package name to install", console.AskOption{
47+
Description: "If no version is specified, install the latest",
48+
Placeholder: " E.g example.com/pkg or example.com/pkg@v1.0.0",
49+
Prompt: ">",
50+
Validate: func(s string) error {
51+
if s == "" {
52+
return errors.CommandEmptyPackageName
53+
}
54+
55+
return nil
56+
},
57+
})
58+
if err != nil {
59+
ctx.Error(err.Error())
60+
return nil
61+
}
62+
}
63+
64+
// handle package version
65+
pkg, version, ok := strings.Cut(pkg, "@")
66+
manager := pkg + "/manager"
67+
if ok {
68+
pkg = pkg + "@" + version
69+
}
70+
71+
// get package
72+
var output []byte
73+
get := exec.Command("go", "get", pkg)
74+
if err = ctx.Spinner(fmt.Sprintf("> @%s", strings.Join(get.Args, " ")), console.SpinnerOption{
75+
Action: func() error {
76+
output, err = get.CombinedOutput()
77+
78+
return err
79+
},
80+
}); err != nil {
81+
color.Errorf("failed to get package: %s\n", err.Error())
82+
if len(output) > 0 {
83+
color.Red().Println(string(output))
84+
}
85+
86+
return nil
87+
}
88+
89+
// install package
90+
install := exec.Command("go", "run", manager, "install")
91+
if err = ctx.Spinner(fmt.Sprintf("> @%s", strings.Join(install.Args, " ")), console.SpinnerOption{
92+
Action: func() error {
93+
output, err = install.CombinedOutput()
94+
95+
return err
96+
},
97+
}); err != nil {
98+
color.Errorf("failed to install package: %s\n", err.Error())
99+
if len(output) > 0 {
100+
color.Red().Println(string(output))
101+
}
102+
103+
return nil
104+
}
105+
106+
// tidy go.mod file
107+
tidy := exec.Command("go", "mod", "tidy")
108+
if err = ctx.Spinner(fmt.Sprintf("> @%s", strings.Join(tidy.Args, " ")), console.SpinnerOption{
109+
Action: func() error {
110+
output, err = tidy.CombinedOutput()
111+
112+
return err
113+
},
114+
}); err != nil {
115+
color.Errorf("failed to tidy go.mod file: %s\n", err.Error())
116+
if len(output) > 0 {
117+
color.Red().Println(string(output))
118+
}
119+
120+
return nil
121+
}
122+
123+
color.Successf("Package %s installed successfully\n", pkg)
124+
125+
return nil
126+
}

foundation/console/package_make_command.go

+16-6
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ func (r *PackageMakeCommand) Extend() command.Extend {
3131
return command.Extend{
3232
Category: "make",
3333
Flags: []command.Flag{
34+
&command.BoolFlag{
35+
Name: "manager",
36+
Aliases: []string{"m"},
37+
Usage: "Create a package manager",
38+
DisableDefaultText: true,
39+
},
3440
&command.StringFlag{
3541
Name: "root",
3642
Aliases: []string{"r"},
@@ -72,12 +78,16 @@ func (r *PackageMakeCommand) Handle(ctx console.Context) error {
7278
packageName := packageName(pkg)
7379
packageMakeCommandStubs := NewPackageMakeCommandStubs(pkg, root)
7480
files := map[string]func() string{
75-
"README.md": packageMakeCommandStubs.Readme,
76-
"service_provider.go": packageMakeCommandStubs.ServiceProvider,
77-
packageName + ".go": packageMakeCommandStubs.Main,
78-
"config/" + packageName + ".go": packageMakeCommandStubs.Config,
79-
"contracts/" + packageName + ".go": packageMakeCommandStubs.Contracts,
80-
"facades/" + packageName + ".go": packageMakeCommandStubs.Facades,
81+
"README.md": packageMakeCommandStubs.Readme,
82+
"service_provider.go": packageMakeCommandStubs.ServiceProvider,
83+
packageName + ".go": packageMakeCommandStubs.Main,
84+
filepath.Join("config", packageName+".go"): packageMakeCommandStubs.Config,
85+
filepath.Join("contracts", packageName+".go"): packageMakeCommandStubs.Contracts,
86+
filepath.Join("facades", packageName+".go"): packageMakeCommandStubs.Facades,
87+
}
88+
89+
if ctx.OptionBool("manager") {
90+
files[filepath.Join("manager", "manager.go")] = packageMakeCommandStubs.Manager
8191
}
8292

8393
for path, content := range files {

foundation/console/package_make_command_stubs.go

+97
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,100 @@ func DummyCamelName() contracts.DummyCamelName {
122122

123123
return content
124124
}
125+
126+
func (r PackageMakeCommandStubs) Manager() string {
127+
content := `package main
128+
129+
import (
130+
"os"
131+
"path"
132+
"path/filepath"
133+
"runtime/debug"
134+
"strings"
135+
136+
pkgcontracts "github.com/goravel/framework/contracts/packages"
137+
"github.com/goravel/framework/packages"
138+
"github.com/goravel/framework/support/color"
139+
)
140+
141+
var (
142+
module string
143+
dir string
144+
force bool
145+
)
146+
147+
func init() {
148+
for i, arg := range os.Args {
149+
if arg == "--force" || arg == "-f" {
150+
force = true
151+
}
152+
153+
if (arg == "--dir" || arg == "-d") && len(os.Args) > i+1 {
154+
dir = os.Args[i+1]
155+
}
156+
}
157+
158+
if info, ok := debug.ReadBuildInfo(); ok && strings.HasSuffix(info.Path, "manager") {
159+
module = path.Dir(info.Path)
160+
}
161+
162+
if dir == "" {
163+
dir, _ = os.Getwd()
164+
}
165+
}
166+
167+
func main() {
168+
var pkg = packages.Manager{
169+
ContinueOnError: force,
170+
Module: module,
171+
OnInstall: []pkgcontracts.FileModifier{
172+
packages.ModifyGoFile{
173+
File: filepath.Join("config", "app.go"),
174+
Modifiers: []pkgcontracts.GoNodeModifier{
175+
packages.AddImportSpec(module),
176+
packages.AddProviderSpec(
177+
"&DummyName.ServiceProvider{}",
178+
),
179+
},
180+
},
181+
},
182+
OnUninstall: []pkgcontracts.FileModifier{
183+
packages.ModifyGoFile{
184+
File: filepath.Join("config", "app.go"),
185+
Modifiers: []pkgcontracts.GoNodeModifier{
186+
packages.RemoveImportSpec(module),
187+
packages.RemoveProviderSpec("&DummyName.ServiceProvider{}"),
188+
},
189+
},
190+
},
191+
}
192+
193+
if module == "" {
194+
color.Errorln("Package module name is empty, please run command with module name.")
195+
return
196+
}
197+
198+
if len(os.Args) > 1 && os.Args[1] == "install" {
199+
err := pkg.Install(dir)
200+
if err != nil {
201+
color.Errorln(err)
202+
return
203+
}
204+
color.Successf("Package %s installed successfully\n", module)
205+
}
206+
207+
if len(os.Args) > 1 && os.Args[1] == "uninstall" {
208+
err := pkg.Uninstall(dir)
209+
if err != nil {
210+
color.Errorln(err)
211+
return
212+
}
213+
color.Successf("Package %s uninstalled successfully\n", module)
214+
}
215+
}
216+
217+
`
218+
content = strings.ReplaceAll(content, "DummyName", r.name)
219+
220+
return content
221+
}

foundation/console/package_make_command_test.go

+38-6
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,12 @@ func (s *PackageMakeCommandTestSuite) TestExtend() {
4242

4343
if len(got.Flags) > 0 {
4444
s.Run("should have correctly configured StringFlag", func() {
45-
flag, ok := got.Flags[0].(*command.StringFlag)
45+
managerFlag, ok := got.Flags[0].(*command.BoolFlag)
46+
if !ok {
47+
s.Fail("First flag is not BoolFlag (got type: %T)", got.Flags[0])
48+
}
49+
50+
rootFlag, ok := got.Flags[1].(*command.StringFlag)
4651
if !ok {
4752
s.Fail("First flag is not StringFlag (got type: %T)", got.Flags[0])
4853
}
@@ -52,10 +57,13 @@ func (s *PackageMakeCommandTestSuite) TestExtend() {
5257
got interface{}
5358
expected interface{}
5459
}{
55-
{"Name", flag.Name, "root"},
56-
{"Aliases", flag.Aliases, []string{"r"}},
57-
{"Usage", flag.Usage, "The root path of package, default: packages"},
58-
{"Value", flag.Value, "packages"},
60+
{"Name", rootFlag.Name, "root"},
61+
{"Aliases", rootFlag.Aliases, []string{"r"}},
62+
{"Usage", rootFlag.Usage, "The root path of package, default: packages"},
63+
{"Value", rootFlag.Value, "packages"},
64+
{"Name", managerFlag.Name, "manager"},
65+
{"Aliases", managerFlag.Aliases, []string{"m"}},
66+
{"Usage", managerFlag.Usage, "Create a package manager"},
5967
}
6068

6169
for _, tc := range testCases {
@@ -93,10 +101,32 @@ func (s *PackageMakeCommandTestSuite) TestHandle() {
93101
},
94102
},
95103
{
96-
name: "name is sms and use default root",
104+
name: "name is sms and use default root(hasn't manager)",
105+
setup: func() {
106+
mockContext.EXPECT().Argument(0).Return("sms").Once()
107+
mockContext.EXPECT().Option("root").Return("packages").Once()
108+
mockContext.EXPECT().OptionBool("manager").Return(false).Once()
109+
mockContext.EXPECT().Success("Package created successfully: packages/sms").Once()
110+
},
111+
assert: func() {
112+
s.NoError(NewPackageMakeCommand().Handle(mockContext))
113+
s.True(file.Exists("packages/sms/README.md"))
114+
s.True(file.Exists("packages/sms/service_provider.go"))
115+
s.True(file.Exists("packages/sms/sms.go"))
116+
s.True(file.Exists("packages/sms/config/sms.go"))
117+
s.True(file.Exists("packages/sms/contracts/sms.go"))
118+
s.True(file.Exists("packages/sms/facades/sms.go"))
119+
s.True(file.Contain("packages/sms/facades/sms.go", "goravel/packages/sms"))
120+
s.True(file.Contain("packages/sms/facades/sms.go", "goravel/packages/sms/contracts"))
121+
s.NoError(file.Remove("packages"))
122+
},
123+
},
124+
{
125+
name: "name is sms and use default root(has manager)",
97126
setup: func() {
98127
mockContext.EXPECT().Argument(0).Return("sms").Once()
99128
mockContext.EXPECT().Option("root").Return("packages").Once()
129+
mockContext.EXPECT().OptionBool("manager").Return(true).Once()
100130
mockContext.EXPECT().Success("Package created successfully: packages/sms").Once()
101131
},
102132
assert: func() {
@@ -109,6 +139,7 @@ func (s *PackageMakeCommandTestSuite) TestHandle() {
109139
s.True(file.Exists("packages/sms/facades/sms.go"))
110140
s.True(file.Contain("packages/sms/facades/sms.go", "goravel/packages/sms"))
111141
s.True(file.Contain("packages/sms/facades/sms.go", "goravel/packages/sms/contracts"))
142+
s.True(file.Exists("packages/sms/manager/manager.go"))
112143
s.NoError(file.Remove("packages"))
113144
},
114145
},
@@ -117,6 +148,7 @@ func (s *PackageMakeCommandTestSuite) TestHandle() {
117148
setup: func() {
118149
mockContext.EXPECT().Argument(0).Return("github.com/goravel/sms-aws").Once()
119150
mockContext.EXPECT().Option("root").Return("package").Once()
151+
mockContext.EXPECT().OptionBool("manager").Return(false).Once()
120152
mockContext.EXPECT().Success("Package created successfully: package/github_com_goravel_sms_aws").Once()
121153
},
122154
assert: func() {

0 commit comments

Comments
 (0)