diff --git a/starport/pkg/cmdrunner/cmdrunner.go b/starport/pkg/cmdrunner/cmdrunner.go index 449468725c..946e1ec7b6 100644 --- a/starport/pkg/cmdrunner/cmdrunner.go +++ b/starport/pkg/cmdrunner/cmdrunner.go @@ -1,13 +1,11 @@ package cmdrunner import ( - "bytes" "context" "io" "os" "os/exec" - "github.com/pkg/errors" "github.com/tendermint/starport/starport/pkg/cmdrunner/step" "golang.org/x/sync/errgroup" ) @@ -189,15 +187,3 @@ func (r *Runner) newCommand(s *step.Step) Executor { } return &cmdSignal{c, w} } - -// Exec executes a command with args, it's a shortcut func for basic command executions. -func Exec(ctx context.Context, command string, args ...string) error { - errb := &bytes.Buffer{} - - err := New( - DefaultStderr(errb)). - Run(ctx, - step.New(step.Exec(command, args...))) - - return errors.Wrap(err, errb.String()) -} diff --git a/starport/pkg/cmdrunner/exec/exec.go b/starport/pkg/cmdrunner/exec/exec.go new file mode 100644 index 0000000000..e12f76d2e7 --- /dev/null +++ b/starport/pkg/cmdrunner/exec/exec.go @@ -0,0 +1,78 @@ +// Package exec provides easy access to command execution for basic uses. +package exec + +import ( + "bytes" + "context" + "fmt" + "strings" + + "github.com/pkg/errors" + "github.com/tendermint/starport/starport/pkg/cmdrunner" + "github.com/tendermint/starport/starport/pkg/cmdrunner/step" +) + +type execConfig struct { + stepOptions []step.Option + includeStdLogsToError bool +} + +type Option func(*execConfig) + +func StepOption(o step.Option) Option { + return func(c *execConfig) { + c.stepOptions = append(c.stepOptions, o) + } +} + +func IncludeStdLogsToError() Option { + return func(c *execConfig) { + c.includeStdLogsToError = true + } +} + +// Exec executes a command with args, it's a shortcut func for basic command executions. +func Exec(ctx context.Context, fullCommand []string, options ...Option) error { + errb := &bytes.Buffer{} + logs := &bytes.Buffer{} + + c := &execConfig{ + stepOptions: []step.Option{ + step.Exec(fullCommand[0], fullCommand[1:]...), + step.Stdout(logs), + step.Stderr(errb), + }, + } + + for _, apply := range options { + apply(c) + } + + err := cmdrunner.New().Run(ctx, step.New(c.stepOptions...)) + if err != nil { + return &Error{ + Err: errors.Wrap(err, errb.String()), + Command: fullCommand[0], + StdLogs: logs.String(), + includeStdLogsToError: c.includeStdLogsToError, + } + } + + return nil +} + +// Error provides detailed errors from the executed program. +type Error struct { + Err error + Command string + StdLogs string // collected logs from code generation tools. + includeStdLogsToError bool +} + +func (e *Error) Error() string { + message := fmt.Sprintf("error while running command %s: %s", e.Command, e.Err.Error()) + if e.includeStdLogsToError && strings.TrimSpace(e.StdLogs) != "" { + return fmt.Sprintf("%s\n\n%s", message, e.StdLogs) + } + return message +} diff --git a/starport/pkg/cosmosanalysis/module/message.go b/starport/pkg/cosmosanalysis/module/message.go new file mode 100644 index 0000000000..0e071f3ae4 --- /dev/null +++ b/starport/pkg/cosmosanalysis/module/message.go @@ -0,0 +1,84 @@ +package module + +import ( + "go/ast" + "go/parser" + "go/token" +) + +// DiscoverMessages discovers sdk messages defined in a module that resides under modulePath. +func DiscoverMessages(modulePath string) (msgs []string, err error) { + // parse go packages/files under modulePath. + fset := token.NewFileSet() + + pkgs, err := parser.ParseDir(fset, modulePath, nil, 0) + if err != nil { + return nil, err + } + + // collect all structs under modulePath to find out the ones that satisfy requirements. + structs := make(map[string]requirements) + + for _, pkg := range pkgs { + for _, f := range pkg.Files { + ast.Inspect(f, func(n ast.Node) bool { + // look for struct methods. + fdecl, ok := n.(*ast.FuncDecl) + if !ok { + return true + } + + // not a method. + if fdecl.Recv == nil { + return true + } + + // fname is the name of method. + fname := fdecl.Name.Name + + // find the struct name that method belongs to. + t := fdecl.Recv.List[0].Type + sident, ok := t.(*ast.Ident) + if !ok { + sexp, ok := t.(*ast.StarExpr) + if !ok { + return true + } + sident = sexp.X.(*ast.Ident) + } + sname := sident.Name + + // mark the requirement that this struct satisfies. + if _, ok := structs[sname]; !ok { + structs[sname] = newRequirements() + } + + structs[sname][fname] = true + + return true + }) + } + } + + // checkRequirements checks if all requirements are satisfied. + checkRequirements := func(r requirements) bool { + for _, ok := range r { + if !ok { + return false + } + } + return true + } + + for name, reqs := range structs { + if checkRequirements(reqs) { + msgs = append(msgs, name) + } + } + + if len(msgs) == 0 { + return nil, ErrModuleNotFound + } + + return msgs, nil +} diff --git a/starport/pkg/cosmosanalysis/module/module.go b/starport/pkg/cosmosanalysis/module/module.go index 754c7ac08d..e516e7280f 100644 --- a/starport/pkg/cosmosanalysis/module/module.go +++ b/starport/pkg/cosmosanalysis/module/module.go @@ -3,9 +3,6 @@ package module import ( "errors" "fmt" - "go/ast" - "go/parser" - "go/token" "path/filepath" "strings" @@ -44,6 +41,12 @@ type Module struct { // Msg is a list of sdk.Msg implementation of the module. Msgs []Msg + + // Queries is a list of module queries. + Queries []Query + + // Types is a list of proto types that might be used by module. + Types []Type } // Msg keeps metadata about an sdk.Msg implementation. @@ -58,6 +61,30 @@ type Msg struct { FilePath string } +// Query is an sdk Query. +type Query struct { + // Name of the RPC func. + Name string + + // FullName of the query with service name and rpc func name. + FullName string + + // HTTPAnnotations keeps info about http annotations of query. + HTTPAnnotations protoanalysis.HTTPAnnotations +} + +// Type is a proto type that might be used by module. +type Type struct { + Name string + + // FilePath is the path of the .proto file where message is defined at. + FilePath string +} + +type moduleDiscoverer struct { + sourcePath, basegopath string +} + // Discover discovers and returns modules and their types that implements sdk.Msg. // sourcePath is the root path of an sdk blockchain. // @@ -74,144 +101,129 @@ func Discover(sourcePath string) ([]Module, error) { if err != nil { return nil, err } - basegopath := gm.Module.Mod.Path + + md := &moduleDiscoverer{ + sourcePath: sourcePath, + basegopath: gm.Module.Mod.Path, + } // find proto packages that belong to modules under x/. - pkgs, err := findModuleProtoPkgs(sourcePath, basegopath) + pkgs, err := md.findModuleProtoPkgs() if err != nil { return nil, err } var modules []Module - // discover discovers and sdk module by a proto pkg. - discover := func(pkg protoanalysis.Package) error { - pkgrelpath := strings.TrimPrefix(pkg.GoImportPath(), basegopath) - pkgpath := filepath.Join(sourcePath, pkgrelpath) - - msgs, err := DiscoverModule(pkgpath) - if err == ErrModuleNotFound { - return nil - } + for _, pkg := range pkgs { + m, err := md.discover(pkg) if err != nil { - return err - } - - var ( - spname = strings.Split(pkg.Name, ".") - m = Module{ - Name: spname[len(spname)-1], - Pkg: pkg, - } - ) - - for _, msg := range msgs { - pkgmsg, err := pkg.MessageByName(msg) - if err != nil { // no msg found in the proto defs corresponds to discovered sdk message. - return nil - } - - m.Msgs = append(m.Msgs, Msg{ - Name: msg, - URI: fmt.Sprintf("%s.%s", pkg.Name, msg), - FilePath: pkgmsg.Path, - }) + return nil, err } modules = append(modules, m) - - return nil - } - - for _, pkg := range pkgs { - if err := discover(pkg); err != nil { - return nil, err - } } return modules, nil } -// DiscoverModule discovers sdk messages defined in a module that resides under modulePath. -func DiscoverModule(modulePath string) (msgs []string, err error) { - // parse go packages/files under modulePath. - fset := token.NewFileSet() +// discover discovers and sdk module by a proto pkg. +func (d *moduleDiscoverer) discover(pkg protoanalysis.Package) (Module, error) { + pkgrelpath := strings.TrimPrefix(pkg.GoImportPath(), d.basegopath) + pkgpath := filepath.Join(d.sourcePath, pkgrelpath) - pkgs, err := parser.ParseDir(fset, modulePath, nil, 0) + msgs, err := DiscoverMessages(pkgpath) + if err == ErrModuleNotFound { + return Module{}, nil + } if err != nil { - return nil, err + return Module{}, err } - // collect all structs under modulePath to find out the ones that satisfy requirements. - structs := make(map[string]requirements) - - for _, pkg := range pkgs { - for _, f := range pkg.Files { - ast.Inspect(f, func(n ast.Node) bool { - // look for struct methods. - fdecl, ok := n.(*ast.FuncDecl) - if !ok { - return true - } - - // not a method. - if fdecl.Recv == nil { - return true - } - - // fname is the name of method. - fname := fdecl.Name.Name - - // find the struct name that method belongs to. - t := fdecl.Recv.List[0].Type - sident, ok := t.(*ast.Ident) - if !ok { - sexp, ok := t.(*ast.StarExpr) - if !ok { - return true - } - sident = sexp.X.(*ast.Ident) - } - sname := sident.Name + namesplit := strings.Split(pkg.Name, ".") + m := Module{ + Name: namesplit[len(namesplit)-1], + Pkg: pkg, + } - // mark the requirement that this struct satisfies. - if _, ok := structs[sname]; !ok { - structs[sname] = newRequirements() - } + // fill sdk Msgs. + for _, msg := range msgs { + pkgmsg, err := pkg.MessageByName(msg) + if err != nil { + // no msg found in the proto defs corresponds to discovered sdk message. + // if it cannot be found, nothing to worry about, this means that it is used + // only internally and not open for actual use. + continue + } - structs[sname][fname] = true + m.Msgs = append(m.Msgs, Msg{ + Name: msg, + URI: fmt.Sprintf("%s.%s", pkg.Name, msg), + FilePath: pkgmsg.Path, + }) + } - return true - }) + // isType whether if protomsg can be added as an any Type to Module. + isType := func(protomsg protoanalysis.Message) bool { + // do not use GenesisState type. + if protomsg.Name == "GenesisState" { + return false } - } - // checkRequirements checks if all requirements are satisfied. - checkRequirements := func(r requirements) bool { - for _, ok := range r { - if !ok { + // do not use if an SDK message. + for _, msg := range msgs { + if msg == protomsg.Name { return false } } + + // do not use if used as a request/return type type of an RPC. + for _, s := range pkg.Services { + for _, q := range s.RPCFuncs { + if q.RequestType == protomsg.Name || q.ReturnsType == protomsg.Name { + return false + } + } + } + return true } - for name, reqs := range structs { - if checkRequirements(reqs) { - msgs = append(msgs, name) + // fill types. + for _, protomsg := range pkg.Messages { + if !isType(protomsg) { + continue } + + m.Types = append(m.Types, Type{ + Name: protomsg.Name, + FilePath: protomsg.Path, + }) } - if len(msgs) == 0 { - return nil, ErrModuleNotFound + // fill queries. + for _, s := range pkg.Services { + for _, q := range s.RPCFuncs { + fullName := s.Name + q.Name + // cannot have a msg and query with the same name. + // if there is, this must be due to there is a Msg service definition. + if _, err := pkg.MessageByName(fullName); err == nil { + continue + } + m.Queries = append(m.Queries, Query{ + Name: q.Name, + FullName: fullName, + HTTPAnnotations: q.HTTPAnnotations, + }) + } } - return msgs, nil + return m, nil } -func findModuleProtoPkgs(sourcePath, bpath string) ([]protoanalysis.Package, error) { +func (d *moduleDiscoverer) findModuleProtoPkgs() ([]protoanalysis.Package, error) { // find out all proto packages inside blockchain. - allprotopkgs, err := protoanalysis.DiscoverPackages(sourcePath) + allprotopkgs, err := protoanalysis.DiscoverPackages(d.sourcePath) if err != nil { return nil, err } @@ -219,7 +231,7 @@ func findModuleProtoPkgs(sourcePath, bpath string) ([]protoanalysis.Package, err // filter out proto packages that do not represent x/ modules of blockchain. var xprotopkgs []protoanalysis.Package for _, pkg := range allprotopkgs { - if !strings.HasPrefix(pkg.GoImportName, bpath) { + if !strings.HasPrefix(pkg.GoImportName, d.basegopath) { continue } diff --git a/starport/pkg/cosmosanalysis/module/module_test.go b/starport/pkg/cosmosanalysis/module/module_test.go index 30a67688ef..72dfd4b789 100644 --- a/starport/pkg/cosmosanalysis/module/module_test.go +++ b/starport/pkg/cosmosanalysis/module/module_test.go @@ -5,36 +5,105 @@ import ( ) func ExampleDiscover() { - pretty.Println(Discover("/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/moon")) + pretty.Println(Discover("/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/mars")) // outputs: // []module.Module{ - // { - // Name: "moon", - // Pkg: protoanalysis.Package{ - // Name: "test.moon.moon", - // Path: "/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/moon/proto/moon", - // GoImportName: "github.com/test/moon/x/moon/types", - // Messages: { - // {Name:"GenesisState", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/moon/proto/moon/genesis.proto"}, - // {Name:"GenesisState", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/moon/proto/moon/genesis.proto"}, - // {Name:"QueryGetUserRequest", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/moon/proto/moon/query.proto"}, - // {Name:"QueryGetUserResponse", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/moon/proto/moon/query.proto"}, - // {Name:"QueryAllUserRequest", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/moon/proto/moon/query.proto"}, - // {Name:"QueryAllUserResponse", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/moon/proto/moon/query.proto"}, - // {Name:"MsgCreateUser", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/moon/proto/moon/tx.proto"}, - // {Name:"MsgCreateUserResponse", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/moon/proto/moon/tx.proto"}, - // {Name:"MsgUpdateUser", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/moon/proto/moon/tx.proto"}, - // {Name:"MsgUpdateUserResponse", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/moon/proto/moon/tx.proto"}, - // {Name:"MsgDeleteUser", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/moon/proto/moon/tx.proto"}, - // {Name:"MsgDeleteUserResponse", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/moon/proto/moon/tx.proto"}, - // {Name:"User", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/moon/proto/moon/user.proto"}, - // }, - // }, - // Msgs: { - // {Name:"MsgUpdateUser", URI:"test.moon.moon.MsgUpdateUser", FilePath:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/moon/proto/moon/tx.proto"}, - // {Name:"MsgDeleteUser", URI:"test.moon.moon.MsgDeleteUser", FilePath:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/moon/proto/moon/tx.proto"}, - // {Name:"MsgCreateUser", URI:"test.moon.moon.MsgCreateUser", FilePath:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/moon/proto/moon/tx.proto"}, - // }, - // }, + // { + // Name: "mars", + // Pkg: protoanalysis.Package{ + // Name: "test.mars.mars", + // Path: "/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/mars/proto/mars", + // GoImportName: "github.com/test/mars/x/mars/types", + // Messages: { + // {Name:"GenesisState", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/mars/proto/mars/genesis.proto"}, + // {Name:"QueryGetUserRequest", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/mars/proto/mars/query.proto"}, + // {Name:"QueryGetUserResponse", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/mars/proto/mars/query.proto"}, + // {Name:"QueryAllUserRequest", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/mars/proto/mars/query.proto"}, + // {Name:"QueryAllUserResponse", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/mars/proto/mars/query.proto"}, + // {Name:"User", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/mars/proto/mars/user.proto"}, + // {Name:"MsgCreateUser", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/mars/proto/mars/tx.proto"}, + // {Name:"MsgCreateUserResponse", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/mars/proto/mars/tx.proto"}, + // {Name:"MsgUpdateUser", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/mars/proto/mars/tx.proto"}, + // {Name:"MsgUpdateUserResponse", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/mars/proto/mars/tx.proto"}, + // {Name:"MsgDeleteUser", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/mars/proto/mars/tx.proto"}, + // {Name:"MsgDeleteUserResponse", Path:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/mars/proto/mars/tx.proto"}, + // }, + // Services: { + // { + // Name: "Query", + // RPCFuncs: { + // { + // Name: "User", + // RequestType: "QueryGetUserRequest", + // ReturnsType: "QueryGetUserResponse", + // HTTPAnnotations: protoanalysis.HTTPAnnotations{ + // URLParams: {"id"}, + // URLHasQuery: false, + // }, + // }, + // { + // Name: "UserAll", + // RequestType: "QueryAllUserRequest", + // ReturnsType: "QueryAllUserResponse", + // HTTPAnnotations: protoanalysis.HTTPAnnotations{ + // URLParams: nil, + // URLHasQuery: true, + // }, + // }, + // }, + // }, + // { + // Name: "Msg", + // RPCFuncs: { + // { + // Name: "CreateUser", + // RequestType: "MsgCreateUser", + // ReturnsType: "MsgCreateUserResponse", + // HTTPAnnotations: protoanalysis.HTTPAnnotations{}, + // }, + // { + // Name: "UpdateUser", + // RequestType: "MsgUpdateUser", + // ReturnsType: "MsgUpdateUserResponse", + // HTTPAnnotations: protoanalysis.HTTPAnnotations{}, + // }, + // { + // Name: "DeleteUser", + // RequestType: "MsgDeleteUser", + // ReturnsType: "MsgDeleteUserResponse", + // HTTPAnnotations: protoanalysis.HTTPAnnotations{}, + // }, + // }, + // }, + // }, + // }, + // Msgs: { + // {Name:"MsgUpdateUser", URI:"test.mars.mars.MsgUpdateUser", FilePath:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/mars/proto/mars/tx.proto"}, + // {Name:"MsgCreateUser", URI:"test.mars.mars.MsgCreateUser", FilePath:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/mars/proto/mars/tx.proto"}, + // {Name:"MsgDeleteUser", URI:"test.mars.mars.MsgDeleteUser", FilePath:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/mars/proto/mars/tx.proto"}, + // }, + // Queries: { + // { + // Name: "User", + // FullName: "QueryUser", + // HTTPAnnotations: protoanalysis.HTTPAnnotations{ + // URLParams: {"id"}, + // URLHasQuery: false, + // }, + // }, + // { + // Name: "UserAll", + // FullName: "QueryUserAll", + // HTTPAnnotations: protoanalysis.HTTPAnnotations{ + // URLParams: nil, + // URLHasQuery: true, + // }, + // }, + // }, + // Types: { + // {Name:"User", FilePath:"/home/ilker/Documents/code/src/github.com/tendermint/starport/local_test/mars/proto/mars/user.proto"}, + // }, + // }, // } nil + } diff --git a/starport/pkg/cosmosgen/cosmosgen.go b/starport/pkg/cosmosgen/cosmosgen.go new file mode 100644 index 0000000000..c67b9bb98d --- /dev/null +++ b/starport/pkg/cosmosgen/cosmosgen.go @@ -0,0 +1,104 @@ +package cosmosgen + +import ( + "context" + + "github.com/tendermint/starport/starport/pkg/cosmosanalysis/module" + gomodmodule "golang.org/x/mod/module" +) + +// generateOptions used to configure code generation. +type generateOptions struct { + includeDirs []string + gomodPath string + jsOut func(module.Module) string + jsIncludeThirdParty bool + vuexStoreRootPath string +} + +// TODO add WithInstall. + +// Option configures code generation. +type Option func(*generateOptions) + +// WithJSGeneration adds JS code generation. out hook is called for each module to +// retrieve the path that should be used to place generated js code inside for a given module. +// if includeThirdPartyModules set to true, code generation will be made for the 3rd party modules +// used by the app -including the SDK- as well. +func WithJSGeneration(includeThirdPartyModules bool, out func(module.Module) (path string)) Option { + return func(o *generateOptions) { + o.jsOut = out + o.jsIncludeThirdParty = includeThirdPartyModules + } +} + +// WithVuexGeneration adds Vuex code generation. storeRootPath is used to determine the root path of generated +// Vuex stores. includeThirdPartyModules and out configures the underlying JS lib generation which is +// documented in WithJSGeneration. +func WithVuexGeneration(includeThirdPartyModules bool, out func(module.Module) (path string), storeRootPath string) Option { + return func(o *generateOptions) { + o.jsOut = out + o.jsIncludeThirdParty = includeThirdPartyModules + o.vuexStoreRootPath = storeRootPath + } +} + +// WithGoGeneration adds Go code generation. +func WithGoGeneration(gomodPath string) Option { + return func(o *generateOptions) { + o.gomodPath = gomodPath + } +} + +// IncludeDirs configures the third party proto dirs that used by app's proto. +// relative to the projectPath. +func IncludeDirs(dirs []string) Option { + return func(o *generateOptions) { + o.includeDirs = dirs + } +} + +// generator generates code for sdk and sdk apps. +type generator struct { + ctx context.Context + appPath string + protoDir string + o *generateOptions + deps []gomodmodule.Version +} + +// Generate generates code from protoDir of an SDK app residing at appPath with given options. +// protoDir must be relative to the projectPath. +func Generate(ctx context.Context, appPath, protoDir string, options ...Option) error { + g := &generator{ + ctx: ctx, + appPath: appPath, + protoDir: protoDir, + o: &generateOptions{}, + } + + for _, apply := range options { + apply(g.o) + } + + if err := g.setup(); err != nil { + return err + } + + if g.o.gomodPath != "" { + if err := g.generateGo(); err != nil { + return err + } + } + + // js generation requires Go types to be existent in the source code. because + // sdk.Msg implementations defined on the generated Go types. + // so it needs to run after Go code gen. + if g.o.jsOut != nil { + if err := g.generateJS(); err != nil { + return err + } + } + + return nil +} diff --git a/starport/pkg/cosmosgen/generate.go b/starport/pkg/cosmosgen/generate.go index e1b791ff5b..d9ab5f2dd6 100644 --- a/starport/pkg/cosmosgen/generate.go +++ b/starport/pkg/cosmosgen/generate.go @@ -1,149 +1,17 @@ package cosmosgen import ( - "context" - "embed" - "io/ioutil" - "os" "path/filepath" "strings" - "text/template" - "github.com/iancoleman/strcase" - "github.com/otiai10/copy" - "github.com/pkg/errors" "github.com/tendermint/starport/starport/pkg/cmdrunner" "github.com/tendermint/starport/starport/pkg/cmdrunner/step" "github.com/tendermint/starport/starport/pkg/cosmosanalysis/module" "github.com/tendermint/starport/starport/pkg/gomodule" - "github.com/tendermint/starport/starport/pkg/nodetime/sta" - tsproto "github.com/tendermint/starport/starport/pkg/nodetime/ts-proto" - "github.com/tendermint/starport/starport/pkg/nodetime/tsc" - "github.com/tendermint/starport/starport/pkg/protoanalysis" - "github.com/tendermint/starport/starport/pkg/protoc" "github.com/tendermint/starport/starport/pkg/protopath" - gomodmodule "golang.org/x/mod/module" - "golang.org/x/sync/errgroup" ) -var ( - goOuts = []string{ - "--gocosmos_out=plugins=interfacetype+grpc,Mgoogle/protobuf/any.proto=github.com/cosmos/cosmos-sdk/codec/types:.", - "--grpc-gateway_out=logtostderr=true:.", - } - - tsOut = []string{ - "--ts_proto_out=.", - } - - openAPIOut = []string{ - "--openapiv2_out=logtostderr=true,allow_merge=true:.", - } - - sdkImport = "github.com/cosmos/cosmos-sdk" -) - -//go:embed templates/* -var templates embed.FS - -// tpl holds the js client template which is for wrapping the generated protobufjs types and rest client, -// utilizing cosmjs' type registry, tx signing & broadcasting through exported, high level txClient() and queryClient() funcs. -func tpl(protoPath string) *template.Template { - return template.Must( - template.New("client.ts.tpl"). - Funcs(template.FuncMap{ - "camelCase": strcase.ToLowerCamel, - "resolveFile": func(fullPath string) string { - rel, _ := filepath.Rel(protoPath, fullPath) - rel = strings.TrimSuffix(rel, ".proto") - return rel - }, - }). - ParseFS(templates, "templates/client.ts.tpl"), - ) -} - -type generateOptions struct { - includeDirs []string - gomodPath string - jsOut func(module.Module) string - jsIncludeThirdParty bool -} - -// TODO add WithInstall. - -// Option configures code generation. -type Option func(*generateOptions) - -// WithJSGeneration adds JS code generation. out hook is called for each module to -// retrieve the path that should be used to place generated js code inside for a given module. -// if includeThirdPartyModules set to true, code generation will be made for the 3rd party modules -// used by the app -including the SDK- as well. -func WithJSGeneration(includeThirdPartyModules bool, out func(module.Module) (path string)) Option { - return func(o *generateOptions) { - o.jsOut = out - o.jsIncludeThirdParty = includeThirdPartyModules - } -} - -// WithGoGeneration adds Go code generation. -func WithGoGeneration(gomodPath string) Option { - return func(o *generateOptions) { - o.gomodPath = gomodPath - } -} - -// IncludeDirs configures the third party proto dirs that used by app's proto. -// relative to the projectPath. -func IncludeDirs(dirs []string) Option { - return func(o *generateOptions) { - o.includeDirs = dirs - } -} - -// generator generates code for sdk and sdk apps. -type generator struct { - ctx context.Context - appPath string - protoDir string - o *generateOptions - deps []gomodmodule.Version -} - -// Generate generates code from protoDir of an SDK app residing at appPath with given options. -// protoDir must be relative to the projectPath. -func Generate(ctx context.Context, appPath, protoDir string, options ...Option) error { - g := &generator{ - ctx: ctx, - appPath: appPath, - protoDir: protoDir, - o: &generateOptions{}, - } - - for _, apply := range options { - apply(g.o) - } - - if err := g.setup(); err != nil { - return err - } - - if g.o.gomodPath != "" { - if err := g.generateGo(); err != nil { - return err - } - } - - // js generation requires Go types to be existent in the source code. - // so it needs to run after Go code gen. - if g.o.jsOut != nil { - if err := g.generateJS(); err != nil { - return err - } - } - - return nil -} +var sdkImport = "github.com/cosmos/cosmos-sdk" func (g *generator) setup() (err error) { // Cosmos SDK hosts proto files of own x/ modules and some third party ones needed by itself and @@ -171,198 +39,6 @@ func (g *generator) setup() (err error) { return } -func (g *generator) generateGo() error { - includePaths, err := g.resolveInclude(g.appPath) - if err != nil { - return err - } - - // created a temporary dir to locate generated code under which later only some of them will be moved to the - // app's source code. this also prevents having leftover files in the app's source code or its parent dir -when - // command executed directly there- in case of an interrupt. - tmp, err := ioutil.TempDir("", "") - if err != nil { - return err - } - defer os.RemoveAll(tmp) - - // discover proto packages in the app. - pp := filepath.Join(g.appPath, g.protoDir) - pkgs, err := protoanalysis.DiscoverPackages(pp) - if err != nil { - return err - } - - // code generate for each module. - for _, pkg := range pkgs { - if err := protoc.Generate(g.ctx, tmp, pkg.Path, includePaths, goOuts); err != nil { - return err - } - } - - // move generated code for the app under the relative locations in its source code. - generatedPath := filepath.Join(tmp, g.o.gomodPath) - - _, err = os.Stat(generatedPath) - if err == nil { - err = copy.Copy(generatedPath, g.appPath) - return errors.Wrap(err, "cannot copy path") - } - if !os.IsNotExist(err) { - return err - } - return nil -} - -func (g *generator) generateJS() error { - tsprotoPluginPath, err := tsproto.BinaryPath() - if err != nil { - return err - } - - // generate generates JS code for a module. - generate := func(ctx context.Context, appPath string, m module.Module) error { - var ( - out = g.o.jsOut(m) - typesOut = filepath.Join(out, "types") - ) - - includePaths, err := g.resolveInclude(appPath) - if err != nil { - return err - } - - // reset destination dir. - if err := os.RemoveAll(out); err != nil { - return err - } - if err := os.MkdirAll(typesOut, 0755); err != nil { - return err - } - - // generate ts-proto types. - err = protoc.Generate( - g.ctx, - typesOut, - m.Pkg.Path, - includePaths, - tsOut, - protoc.Plugin(tsprotoPluginPath), - ) - if err != nil { - return err - } - - // generate OpenAPI spec. - oaitemp, err := ioutil.TempDir("", "") - if err != nil { - return err - } - defer os.RemoveAll(oaitemp) - - err = protoc.Generate( - ctx, - oaitemp, - m.Pkg.Path, - includePaths, - openAPIOut, - ) - if err != nil { - return err - } - - // generate the REST client from the OpenAPI spec. - var ( - srcspec = filepath.Join(oaitemp, "apidocs.swagger.json") - outREST = filepath.Join(out, "rest.ts") - ) - - if err := sta.Generate(g.ctx, outREST, srcspec, "-1"); err != nil { // -1 removes the route namespace. - return err - } - - // generate the js client wrapper. - outclient := filepath.Join(out, "index.ts") - f, err := os.OpenFile(outclient, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) - if err != nil { - return err - } - defer f.Close() - - pp := filepath.Join(appPath, g.protoDir) - err = tpl(pp).Execute(f, struct{ Module module.Module }{m}) - if err != nil { - return err - } - - // generate .js and .d.ts files for all ts files. - err = tsc.Generate(g.ctx, tsc.Config{ - Include: []string{out + "/**/*.ts"}, - CompilerOptions: tsc.CompilerOptions{ - Declaration: true, - }, - }) - - return err - } - - // sourcePaths keeps a list of root paths of Go projects (source codes) that might contain - // Cosmos SDK modules inside. - sourcePaths := []string{ - g.appPath, // user's blockchain. may contain internal modules. it is the first place to look for. - } - - if g.o.jsIncludeThirdParty { - // go through the Go dependencies (inside go.mod) of each source path, some of them might be hosting - // Cosmos SDK modules that could be in use by user's blockchain. - // - // Cosmos SDK is a dependency of all blockchains, so it's absolute that we'll be discovering all modules of the - // SDK as well during this process. - // - // even if a dependency contains some SDK modules, not all of these modules could be used by user's blockchain. - // this is fine, we can still generate JS clients for those non modules, it is up to user to use (import in JS) - // not use generated modules. - // not used ones will never get resolved inside JS environment and will not ship to production, JS bundlers will avoid. - // - // TODO(ilgooz): we can still implement some sort of smart filtering to detect non used modules by the user's blockchain - // at some point, it is a nice to have. - for _, dep := range g.deps { - deppath, err := gomodule.LocatePath(dep) - if err != nil { - return err - } - sourcePaths = append(sourcePaths, deppath) - } - } - - gs := &errgroup.Group{} - - // try to discover SDK modules in all source paths. - for _, sourcePath := range sourcePaths { - sourcePath := sourcePath - - gs.Go(func() error { - modules, err := g.discoverModules(sourcePath) - if err != nil { - return err - } - - gg, ctx := errgroup.WithContext(g.ctx) - - // do code generation for each found module. - for _, m := range modules { - m := m - - gg.Go(func() error { return generate(ctx, sourcePath, m) }) - } - - return gg.Wait() - }) - } - - return gs.Wait() -} - func (g *generator) resolveInclude(path string) (paths []string, err error) { paths = append(paths, filepath.Join(path, g.protoDir)) for _, p := range g.o.includeDirs { diff --git a/starport/pkg/cosmosgen/generate_go.go b/starport/pkg/cosmosgen/generate_go.go new file mode 100644 index 0000000000..1cafd436e2 --- /dev/null +++ b/starport/pkg/cosmosgen/generate_go.go @@ -0,0 +1,62 @@ +package cosmosgen + +import ( + "io/ioutil" + "os" + "path/filepath" + + "github.com/otiai10/copy" + "github.com/pkg/errors" + "github.com/tendermint/starport/starport/pkg/protoanalysis" + "github.com/tendermint/starport/starport/pkg/protoc" +) + +var ( + goOuts = []string{ + "--gocosmos_out=plugins=interfacetype+grpc,Mgoogle/protobuf/any.proto=github.com/cosmos/cosmos-sdk/codec/types:.", + "--grpc-gateway_out=logtostderr=true:.", + } +) + +func (g *generator) generateGo() error { + includePaths, err := g.resolveInclude(g.appPath) + if err != nil { + return err + } + + // created a temporary dir to locate generated code under which later only some of them will be moved to the + // app's source code. this also prevents having leftover files in the app's source code or its parent dir -when + // command executed directly there- in case of an interrupt. + tmp, err := ioutil.TempDir("", "") + if err != nil { + return err + } + defer os.RemoveAll(tmp) + + // discover proto packages in the app. + pp := filepath.Join(g.appPath, g.protoDir) + pkgs, err := protoanalysis.DiscoverPackages(pp) + if err != nil { + return err + } + + // code generate for each module. + for _, pkg := range pkgs { + if err := protoc.Generate(g.ctx, tmp, pkg.Path, includePaths, goOuts); err != nil { + return err + } + } + + // move generated code for the app under the relative locations in its source code. + generatedPath := filepath.Join(tmp, g.o.gomodPath) + + _, err = os.Stat(generatedPath) + if err == nil { + err = copy.Copy(generatedPath, g.appPath) + return errors.Wrap(err, "cannot copy path") + } + if !os.IsNotExist(err) { + return err + } + return nil +} diff --git a/starport/pkg/cosmosgen/generate_javascript.go b/starport/pkg/cosmosgen/generate_javascript.go new file mode 100644 index 0000000000..5f407231cf --- /dev/null +++ b/starport/pkg/cosmosgen/generate_javascript.go @@ -0,0 +1,255 @@ +package cosmosgen + +import ( + "context" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/iancoleman/strcase" + "github.com/mattn/go-zglob" + "github.com/tendermint/starport/starport/pkg/cosmosanalysis/module" + "github.com/tendermint/starport/starport/pkg/gomodule" + "github.com/tendermint/starport/starport/pkg/nodetime/sta" + tsproto "github.com/tendermint/starport/starport/pkg/nodetime/ts-proto" + "github.com/tendermint/starport/starport/pkg/nodetime/tsc" + "github.com/tendermint/starport/starport/pkg/protoc" + "golang.org/x/sync/errgroup" +) + +var ( + tsOut = []string{ + "--ts_proto_out=.", + } + + openAPIOut = []string{ + "--openapiv2_out=logtostderr=true,allow_merge=true:.", + } + + vuexRootMarker = "vuex-root" +) + +type jsGenerator struct { + g *generator + tsprotoPluginPath string +} + +func newJSGenerator(g *generator) (jsGenerator, error) { + tsprotoPluginPath, err := tsproto.BinaryPath() + if err != nil { + return jsGenerator{}, err + } + + return jsGenerator{ + g: g, + tsprotoPluginPath: tsprotoPluginPath, + }, nil +} + +func (g *generator) generateJS() error { + jsg, err := newJSGenerator(g) + if err != nil { + return err + } + + if err := jsg.generateModules(); err != nil { + return err + } + + if err := jsg.generateVuexModuleLoader(); err != nil { + return err + } + + return nil +} + +func (g *jsGenerator) generateModules() error { + // sourcePaths keeps a list of root paths of Go projects (source codes) that might contain + // Cosmos SDK modules inside. + sourcePaths := []string{ + g.g.appPath, // user's blockchain. may contain internal modules. it is the first place to look for. + } + + if g.g.o.jsIncludeThirdParty { + // go through the Go dependencies (inside go.mod) of each source path, some of them might be hosting + // Cosmos SDK modules that could be in use by user's blockchain. + // + // Cosmos SDK is a dependency of all blockchains, so it's absolute that we'll be discovering all modules of the + // SDK as well during this process. + // + // even if a dependency contains some SDK modules, not all of these modules could be used by user's blockchain. + // this is fine, we can still generate JS clients for those non modules, it is up to user to use (import in JS) + // not use generated modules. + // not used ones will never get resolved inside JS environment and will not ship to production, JS bundlers will avoid. + // + // TODO(ilgooz): we can still implement some sort of smart filtering to detect non used modules by the user's blockchain + // at some point, it is a nice to have. + for _, dep := range g.g.deps { + deppath, err := gomodule.LocatePath(dep) + if err != nil { + return err + } + sourcePaths = append(sourcePaths, deppath) + } + } + + gs := &errgroup.Group{} + + // try to discover SDK modules in all source paths. + for _, sourcePath := range sourcePaths { + sourcePath := sourcePath + + gs.Go(func() error { + modules, err := g.g.discoverModules(sourcePath) + if err != nil { + return err + } + + gg, ctx := errgroup.WithContext(g.g.ctx) + + // do code generation for each found module. + for _, m := range modules { + m := m + + gg.Go(func() error { return g.generateModule(ctx, g.tsprotoPluginPath, sourcePath, m) }) + } + + return gg.Wait() + }) + } + + return gs.Wait() +} + +// generateModule generates generates JS code for a module. +func (g *jsGenerator) generateModule(ctx context.Context, tsprotoPluginPath, appPath string, m module.Module) error { + var ( + out = g.g.o.jsOut(m) + storeDirPath = filepath.Dir(out) + typesOut = filepath.Join(out, "types") + ) + + includePaths, err := g.g.resolveInclude(appPath) + if err != nil { + return err + } + + // reset destination dir. + if err := os.RemoveAll(out); err != nil { + return err + } + if err := os.MkdirAll(typesOut, 0755); err != nil { + return err + } + + // generate ts-proto types. + err = protoc.Generate( + g.g.ctx, + typesOut, + m.Pkg.Path, + includePaths, + tsOut, + protoc.Plugin(tsprotoPluginPath), + ) + if err != nil { + return err + } + + // generate OpenAPI spec. + oaitemp, err := ioutil.TempDir("", "") + if err != nil { + return err + } + defer os.RemoveAll(oaitemp) + + err = protoc.Generate( + ctx, + oaitemp, + m.Pkg.Path, + includePaths, + openAPIOut, + ) + if err != nil { + return err + } + + // generate the REST client from the OpenAPI spec. + var ( + srcspec = filepath.Join(oaitemp, "apidocs.swagger.json") + outREST = filepath.Join(out, "rest.ts") + ) + + if err := sta.Generate(g.g.ctx, outREST, srcspec, "-1"); err != nil { // -1 removes the route namespace. + return err + } + + // generate the js client wrapper. + pp := filepath.Join(appPath, g.g.protoDir) + if err := templateJSClient.Write(out, pp, struct{ Module module.Module }{m}); err != nil { + return err + } + + // generate Vuex if enabled. + if g.g.o.vuexStoreRootPath != "" { + err = templateVuexStore.Write(storeDirPath, pp, struct{ Module module.Module }{m}) + if err != nil { + return err + } + } + + // generate .js and .d.ts files for all ts files. + return tsc.Generate(g.g.ctx, tscConfig(storeDirPath+"/**/*.ts")) +} + +func (g *jsGenerator) generateVuexModuleLoader() error { + modulePaths, err := zglob.Glob(filepath.Join(g.g.o.vuexStoreRootPath, "/**/"+vuexRootMarker)) + if err != nil { + return err + } + + type module struct { + Name string + Path string + FullName string + FullPath string + } + + var modules []module + + for _, path := range modulePaths { + pathrel, err := filepath.Rel(g.g.o.vuexStoreRootPath, path) + if err != nil { + return err + } + var ( + fullPath = filepath.Dir(pathrel) + fullName = strcase.ToCamel(strings.ReplaceAll(fullPath, "/", "_")) + path = filepath.Base(fullPath) + name = strcase.ToCamel(path) + ) + modules = append(modules, module{ + Name: name, + Path: path, + FullName: fullName, + FullPath: fullPath, + }) + } + + loaderPath := filepath.Join(g.g.o.vuexStoreRootPath, "index.ts") + + if err := templateVuexRoot.Write(g.g.o.vuexStoreRootPath, "", modules); err != nil { + return err + } + + return tsc.Generate(g.g.ctx, tscConfig(loaderPath)) +} + +func tscConfig(include ...string) tsc.Config { + return tsc.Config{ + Include: include, + CompilerOptions: tsc.CompilerOptions{ + Declaration: true, + }, + } +} diff --git a/starport/pkg/cosmosgen/template.go b/starport/pkg/cosmosgen/template.go new file mode 100644 index 0000000000..4d45111556 --- /dev/null +++ b/starport/pkg/cosmosgen/template.go @@ -0,0 +1,85 @@ +package cosmosgen + +import ( + "embed" + "os" + "path/filepath" + "strings" + "text/template" + + "github.com/iancoleman/strcase" +) + +var ( + //go:embed templates/* + templates embed.FS + + templateJSClient = newTemplateWriter("js") // js wrapper client. + templateVuexRoot = newTemplateWriter("vuex/root") // vuex store loader. + templateVuexStore = newTemplateWriter("vuex/store") // vuex store. +) + +type templateWriter struct { + templateDir string +} + +// tpl returns a func for template residing at templatePath to initialize a text template +// with given protoPath. +func newTemplateWriter(templateDir string) templateWriter { + return templateWriter{ + templateDir, + } +} + +func (t templateWriter) Write(destDir, protoPath string, data interface{}) error { + base := filepath.Join("templates", t.templateDir) + + // find out templates inside the dir. + files, err := templates.ReadDir(base) + if err != nil { + return err + } + + var paths []string + for _, file := range files { + paths = append(paths, filepath.Join(base, file.Name())) + } + + funcs := template.FuncMap{ + "camelCase": strcase.ToLowerCamel, + "resolveFile": func(fullPath string) string { + rel, _ := filepath.Rel(protoPath, fullPath) + rel = strings.TrimSuffix(rel, ".proto") + return rel + }, + } + + // render and write the template. + write := func(path string) error { + tpl := template. + Must( + template. + New(filepath.Base(path)). + Funcs(funcs). + ParseFS(templates, paths...), + ) + + out := filepath.Join(destDir, strings.TrimSuffix(filepath.Base(path), ".tpl")) + + f, err := os.OpenFile(out, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return err + } + defer f.Close() + + return tpl.Execute(f, data) + } + + for _, path := range paths { + if err := write(path); err != nil { + return err + } + } + + return nil +} diff --git a/starport/pkg/cosmosgen/templates/client.ts.tpl b/starport/pkg/cosmosgen/templates/js/index.ts.tpl similarity index 79% rename from starport/pkg/cosmosgen/templates/client.ts.tpl rename to starport/pkg/cosmosgen/templates/js/index.ts.tpl index 588d655163..a5ccf8e0fe 100644 --- a/starport/pkg/cosmosgen/templates/client.ts.tpl +++ b/starport/pkg/cosmosgen/templates/js/index.ts.tpl @@ -1,4 +1,6 @@ -import { coins, StdFee } from "@cosmjs/launchpad"; +// THIS FILE IS GENERATED AUTOMATICALLY. DO NOT MODIFY. + +import { StdFee } from "@cosmjs/launchpad"; import { SigningStargateClient } from "@cosmjs/stargate"; import { Registry, OfflineSigner, EncodeObject, DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; import { Api } from "./rest"; @@ -22,7 +24,8 @@ interface TxClientOptions { } interface SignAndBroadcastOptions { - fee: StdFee + fee: StdFee, + memo?: string } const txClient = async (wallet: OfflineSigner, { addr: addr }: TxClientOptions = { addr: "http://localhost:26657" }) => { @@ -32,7 +35,7 @@ const txClient = async (wallet: OfflineSigner, { addr: addr }: TxClientOptions = const { address } = (await wallet.getAccounts())[0]; return { - signAndBroadcast: (msgs: EncodeObject[], { fee: fee }: SignAndBroadcastOptions = { fee: defaultFee }) => client.signAndBroadcast(address, msgs, fee), + signAndBroadcast: (msgs: EncodeObject[], { fee=defaultFee, memo=null }: SignAndBroadcastOptions) => memo?client.signAndBroadcast(address, msgs, fee,memo):client.signAndBroadcast(address, msgs, fee), {{ range .Module.Msgs }}{{ camelCase .Name }}: (data: {{ .Name }}): EncodeObject => ({ typeUrl: "/{{ .URI }}", value: data }), {{ end }} }; diff --git a/starport/pkg/cosmosgen/templates/vuex/root/index.ts.tpl b/starport/pkg/cosmosgen/templates/vuex/root/index.ts.tpl new file mode 100644 index 0000000000..7505baa434 --- /dev/null +++ b/starport/pkg/cosmosgen/templates/vuex/root/index.ts.tpl @@ -0,0 +1,34 @@ +// THIS FILE IS GENERATED AUTOMATICALLY. DO NOT MODIFY. + +{{ range . }}import {{ .FullName }} from './{{ .FullPath }}' +{{ end }} + +export default { + {{ range . }}{{ .FullName }}: load({{ .FullName }}, '{{ .Path }}'), + {{ end }} +} + + +function load(mod, fullns) { + return function init(store) { + const fullnsLevels = fullns.split('/') + for (let i = 1; i < fullnsLevels.length; i++) { + let ns = fullnsLevels.slice(0, i) + if (!store.hasModule(ns)) { + store.registerModule(ns, { namespaced: true }) + } + } + if (store.hasModule(fullnsLevels)) { + throw new Error('Duplicate module name detected: '+ fullnsLevels.pop()) + }else{ + store.registerModule(fullnsLevels, mod) + store.subscribe((mutation) => { + if (mutation.type == 'common/env/INITIALIZE_WS_COMPLETE') { + store.dispatch(fullns+ '/init', null, { + root: true + }) + } + }) + } + } +} diff --git a/starport/pkg/cosmosgen/templates/vuex/root/readme.md b/starport/pkg/cosmosgen/templates/vuex/root/readme.md new file mode 100644 index 0000000000..a9a292f4b9 --- /dev/null +++ b/starport/pkg/cosmosgen/templates/vuex/root/readme.md @@ -0,0 +1 @@ +THIS FOLDER IS GENERATED AUTOMATICALLY. DO NOT MODIFY. diff --git a/starport/pkg/cosmosgen/templates/vuex/store/index.ts.tpl b/starport/pkg/cosmosgen/templates/vuex/store/index.ts.tpl new file mode 100644 index 0000000000..0880ed6aec --- /dev/null +++ b/starport/pkg/cosmosgen/templates/vuex/store/index.ts.tpl @@ -0,0 +1,149 @@ +import { txClient, queryClient } from './module' +// @ts-ignore +import { SpVuexError } from '@starport/vuex' + +{{ range .Module.Types }}import { {{ .Name }} } from "./module/types/{{ resolveFile .FilePath }}" +{{ end }} + +async function initTxClient(vuexGetters) { + return await txClient(vuexGetters['common/wallet/signer'], { + addr: vuexGetters['common/env/apiTendermint'] + }) +} + +async function initQueryClient(vuexGetters) { + return await queryClient({ + addr: vuexGetters['common/env/apiCosmos'] + }) +} + +function getStructure(template) { + let structure = { fields: [] } + for (const [key, value] of Object.entries(template)) { + let field: any = {} + field.name = key + field.type = typeof value + structure.fields.push(field) + } + return structure +} + +const getDefaultState = () => { + return { + {{ range .Module.Queries }}{{ .Name }}: {}, + {{ end }} + _Structure: { + {{ range .Module.Types }}{{ .Name }}: getStructure({{ .Name }}.fromPartial({})), + {{ end }} + }, + _Subscriptions: new Set(), + } +} + +// initial state +const state = getDefaultState() + +export default { + namespaced: true, + state, + mutations: { + RESET_STATE(state) { + Object.assign(state, getDefaultState()) + }, + QUERY(state, { query, key, value }) { + state[query][JSON.stringify(key)] = value + }, + SUBSCRIBE(state, subscription) { + state._Subscriptions.add(subscription) + }, + UNSUBSCRIBE(state, subscription) { + state._Subscriptions.delete(subscription) + } + }, + getters: { + {{ range .Module.Queries }}get{{ .Name }}: (state) => (params = {}) => { + if (!( params).query) { + ( params).query=null + } + return state.{{ .Name }}[JSON.stringify(params)] ?? {} + }, + {{ end }} + getTypeStructure: (state) => (type) => { + return state._Structure[type].fields + } + }, + actions: { + init({ dispatch, rootGetters }) { + console.log('init') + if (rootGetters['common/env/client']) { + rootGetters['common/env/client'].on('newblock', () => { + dispatch('StoreUpdate') + }) + } + }, + resetState({ commit }) { + commit('RESET_STATE') + }, + unsubscribe({ commit }, subscription) { + commit('UNSUBSCRIBE', subscription) + }, + async StoreUpdate({ state, dispatch }) { + state._Subscriptions.forEach((subscription) => { + dispatch(subscription.action, subscription.payload) + }) + }, + {{ range .Module.Queries }}async {{ .FullName }}({ commit, rootGetters, getters }, { options: { subscribe = false , all = false}, params: {...key}, query=null }) { + try { + + let value = query?(await (await initQueryClient(rootGetters)).{{ camelCase .FullName }}({{ range $i,$a :=.HTTPAnnotations.URLParams}} key.{{$a}}, {{end}} query)).data:(await (await initQueryClient(rootGetters)).{{ camelCase .FullName }}({{ range $i,$a :=.HTTPAnnotations.URLParams}}{{ if (gt $i 0)}}, {{ end}} key.{{$a}} {{end}})).data + {{ if .HTTPAnnotations.URLHasQuery}} + while (all && ( value).pagination && ( value).pagination.nextKey!=null) { + let next_values=(await (await initQueryClient(rootGetters)).{{ camelCase .FullName }}({{ range $i,$a :=.HTTPAnnotations.URLParams}} key.{{$a}}, {{end}}{...query, 'pagination.key':( value).pagination.nextKey})).data + for (let prop of Object.keys(next_values)) { + if (Array.isArray(next_values[prop])) { + value[prop]=[...value[prop], ...next_values[prop]] + }else{ + value[prop]=next_values[prop] + } + } + } + {{ end }} + commit('QUERY', { query: '{{ .Name }}', key: { params: {...key}, query}, value }) + if (subscribe) commit('SUBSCRIBE', { action: '{{ .FullName }}', payload: { options: { all }, params: {...key},query }}) + return getters['get{{.Name }}']( { params: {...key}, query}) ?? {} + } catch (e) { + console.error(new SpVuexError('QueryClient:{{ .FullName }}', 'API Node Unavailable. Could not perform query.')) + return {} + } + }, + {{ end }} + {{ range .Module.Msgs }}async send{{ .Name }}({ rootGetters }, { value, fee, memo }) { + try { + const msg = await (await initTxClient(rootGetters)).{{ camelCase .Name }}(value) + const result = await (await initTxClient(rootGetters)).signAndBroadcast([msg], {fee: { amount: fee, + gas: "200000" }, memo}) + return result + } catch (e) { + if (e.toString()=='wallet is required') { + throw new SpVuexError('TxClient:{{ .Name }}:Init', 'Could not initialize signing client. Wallet is required.') + }else{ + throw new SpVuexError('TxClient:{{ .Name }}:Send', 'Could not broadcast Tx.') + } + } + }, + {{ end }} + {{ range .Module.Msgs }}async {{ .Name }}({ rootGetters }, { value }) { + try { + const msg = await (await initTxClient(rootGetters)).{{ camelCase .Name }}(value) + return msg + } catch (e) { + if (e.toString()=='wallet is required') { + throw new SpVuexError('TxClient:{{ .Name }}:Init', 'Could not initialize signing client. Wallet is required.') + }else{ + throw new SpVuexError('TxClient:{{ .Name }}:Create', 'Could not create message.') + } + } + }, + {{ end }} + } +} diff --git a/starport/pkg/cosmosgen/templates/vuex/store/vuex-root b/starport/pkg/cosmosgen/templates/vuex/store/vuex-root new file mode 100644 index 0000000000..0fcc121a15 --- /dev/null +++ b/starport/pkg/cosmosgen/templates/vuex/store/vuex-root @@ -0,0 +1 @@ +THIS FILE IS GENERATED AUTOMATICALLY. DO NOT DELETE. diff --git a/starport/pkg/nodetime/sta/sta.go b/starport/pkg/nodetime/sta/sta.go index 509973212d..017a794d02 100644 --- a/starport/pkg/nodetime/sta/sta.go +++ b/starport/pkg/nodetime/sta/sta.go @@ -6,7 +6,7 @@ import ( "path/filepath" "sync" - "github.com/tendermint/starport/starport/pkg/cmdrunner" + "github.com/tendermint/starport/starport/pkg/cmdrunner/exec" "github.com/tendermint/starport/starport/pkg/nodetime" ) @@ -41,5 +41,5 @@ func Generate(ctx context.Context, outPath, specPath, moduleNameIndex string) er } // execute the command. - return cmdrunner.Exec(ctx, command[0], command[1:]...) + return exec.Exec(ctx, command, exec.IncludeStdLogsToError()) } diff --git a/starport/pkg/nodetime/tsc/tsc.go b/starport/pkg/nodetime/tsc/tsc.go index c242ada067..9c7b789012 100644 --- a/starport/pkg/nodetime/tsc/tsc.go +++ b/starport/pkg/nodetime/tsc/tsc.go @@ -7,7 +7,7 @@ import ( "sync" "github.com/imdario/mergo" - "github.com/tendermint/starport/starport/pkg/cmdrunner" + "github.com/tendermint/starport/starport/pkg/cmdrunner/exec" "github.com/tendermint/starport/starport/pkg/confile" "github.com/tendermint/starport/starport/pkg/nodetime" ) @@ -89,5 +89,5 @@ func Generate(ctx context.Context, config Config) error { } // execute the command. - return cmdrunner.Exec(ctx, command[0], command[1:]...) + return exec.Exec(ctx, command, exec.IncludeStdLogsToError()) } diff --git a/starport/pkg/protoanalysis/protoanalysis.go b/starport/pkg/protoanalysis/protoanalysis.go index c0ef90784c..19f51e99ba 100644 --- a/starport/pkg/protoanalysis/protoanalysis.go +++ b/starport/pkg/protoanalysis/protoanalysis.go @@ -5,6 +5,7 @@ import ( "errors" "os" "path/filepath" + "regexp" "strings" "sync" @@ -30,6 +31,42 @@ type Package struct { // Messages is a list of proto messages defined in the package. Messages []Message + + // Services is a list of RPC services. + Services []Service +} + +// Service is an RPC service. +type Service struct { + // Name of the services. + Name string + + // RPCFuncs is a list of RPC funcs of the service. + RPCFuncs []RPCFunc +} + +// RPCFunc is an RPC func. +type RPCFunc struct { + // Name of the RPC func. + Name string + + // RequestType is the request type of RPC func. + RequestType string + + // ReturnsType is the response type of RPC func. + ReturnsType string + + // HTTPAnnotations keeps info about http annotations of an RPC func. + HTTPAnnotations HTTPAnnotations +} + +// HTTPAnnotations keeps info about http annotations of an RPC func. +type HTTPAnnotations struct { + // URLParams is a list of parameters defined in the http endpoint annotation. + URLParams []string + + // URLHasQuery indicates if query parameters can be passed in the gRPC Gatweway mode. + URLHasQuery bool } // MessageByName finds a message by its name inside Package. @@ -95,9 +132,10 @@ func DiscoverPackages(path string) ([]Package, error) { } if !exists { pkgs = append(pkgs, pkg) - index = len(pkgs) - 1 + } else { + pkgs[index].Messages = append(pkgs[index].Messages, pkg.Messages...) + pkgs[index].Services = append(pkgs[index].Services, pkg.Services...) } - pkgs[index].Messages = append(pkgs[index].Messages, pkg.Messages...) return nil }) @@ -106,6 +144,8 @@ func DiscoverPackages(path string) ([]Package, error) { return pkgs, g.Wait() } +var urlParamRe = regexp.MustCompile(`(?m){(\w+)}`) + // Parse parses a proto file residing at path. func Parse(path string) (Package, error) { f, err := os.Open(path) @@ -123,6 +163,11 @@ func Parse(path string) (Package, error) { Path: filepath.Dir(path), } + var ( + messages []*proto.Message + services []*proto.Service + ) + proto.Walk( def, proto.WithPackage(func(p *proto.Package) { pkg.Name = p.Name }), @@ -133,13 +178,80 @@ func Parse(path string) (Package, error) { pkg.GoImportName = o.Constant.Source }), proto.WithMessage(func(m *proto.Message) { - pkg.Messages = append(pkg.Messages, Message{ - Name: m.Name, - Path: path, - }) + messages = append(messages, m) + }), + proto.WithService(func(s *proto.Service) { + services = append(services, s) }), ) + for _, m := range messages { + pkg.Messages = append(pkg.Messages, Message{ + Name: m.Name, + Path: path, + }) + } + + for _, s := range services { + sv := Service{ + Name: s.Name, + } + + for _, el := range s.Elements { + rpc, ok := el.(*proto.RPC) + if !ok { + continue + } + + rpcFunc := RPCFunc{ + Name: rpc.Name, + RequestType: rpc.RequestType, + ReturnsType: rpc.ReturnsType, + } + + // check for http annotations and collect info about them. + for _, el := range rpc.Elements { + option, ok := el.(*proto.Option) + if !ok { + continue + } + if !strings.Contains(option.Name, "google.api.http") { + continue + } + + // fill url params. + match := urlParamRe.FindAllStringSubmatch(option.Constant.Source, -1) + for _, item := range match { + rpcFunc.HTTPAnnotations.URLParams = append(rpcFunc.HTTPAnnotations.URLParams, item[1]) + } + + // fill has query params. + for _, m := range messages { + if m.Name != rpc.RequestType { + continue + } + + var fieldCount int + for _, el := range m.Elements { + switch el.(type) { + case + *proto.NormalField, + *proto.MapField, + *proto.OneOfField: + fieldCount++ + } + } + + rpcFunc.HTTPAnnotations.URLHasQuery = fieldCount > len(rpcFunc.HTTPAnnotations.URLParams) + } + + } + sv.RPCFuncs = append(sv.RPCFuncs, rpcFunc) + } + + pkg.Services = append(pkg.Services, sv) + } + return pkg, nil } diff --git a/starport/pkg/protoc/protoc.go b/starport/pkg/protoc/protoc.go index 984c911212..4896b84c43 100644 --- a/starport/pkg/protoc/protoc.go +++ b/starport/pkg/protoc/protoc.go @@ -2,12 +2,10 @@ package protoc import ( - "bytes" "context" "os" - "github.com/pkg/errors" - "github.com/tendermint/starport/starport/pkg/cmdrunner" + "github.com/tendermint/starport/starport/pkg/cmdrunner/exec" "github.com/tendermint/starport/starport/pkg/cmdrunner/step" "github.com/tendermint/starport/starport/pkg/protoanalysis" ) @@ -64,17 +62,11 @@ func Generate(ctx context.Context, outDir, protoPath string, includePaths, proto command := append(command, out) command = append(command, files...) - errb := &bytes.Buffer{} - - err := cmdrunner. - New( - cmdrunner.DefaultStderr(errb), - cmdrunner.DefaultWorkdir(outDir)). - Run(ctx, - step.New(step.Exec(command[0], command[1:]...))) - - if err != nil { - return errors.Wrap(err, errb.String()) + if err := exec.Exec(ctx, command, + exec.StepOption(step.Workdir(outDir)), + exec.IncludeStdLogsToError(), + ); err != nil { + return err } } diff --git a/starport/services/chain/build.go b/starport/services/chain/build.go index 4fefea14e8..3757d49ad6 100644 --- a/starport/services/chain/build.go +++ b/starport/services/chain/build.go @@ -173,9 +173,16 @@ func (c *Chain) buildProto(ctx context.Context) error { // generate Vuex code as well if it is enabled. if conf.Client.Vuex.Path != "" { - options = append(options, cosmosgen.WithJSGeneration(enableThirdPartyModuleCodegen, func(m module.Module) string { - return filepath.Join(c.app.Path, conf.Client.Vuex.Path, "chain", giturl.UserAndRepo(m.Pkg.GoImportName), m.Pkg.Name, "module") - })) + storeRootPath := filepath.Join(c.app.Path, conf.Client.Vuex.Path, "generated") + options = append(options, + cosmosgen.WithVuexGeneration( + enableThirdPartyModuleCodegen, + func(m module.Module) string { + return filepath.Join(storeRootPath, giturl.UserAndRepo(m.Pkg.GoImportName), m.Pkg.Name, "module") + }, + storeRootPath, + ), + ) } if err := cosmosgen.Generate(ctx, c.app.Path, conf.Build.Proto.Path, options...); err != nil { diff --git a/starport/services/scaffolder/init.go b/starport/services/scaffolder/init.go index 8d81267d86..1a7f34a931 100644 --- a/starport/services/scaffolder/init.go +++ b/starport/services/scaffolder/init.go @@ -108,9 +108,16 @@ func (s *Scaffolder) protoc(projectPath, gomodPath string, version cosmosver.Maj // generate Vuex code as well if it is enabled. if conf.Client.Vuex.Path != "" { - options = append(options, cosmosgen.WithJSGeneration(false, func(m module.Module) string { - return filepath.Join(projectPath, conf.Client.Vuex.Path, "chain", giturl.UserAndRepo(m.Pkg.GoImportName), m.Pkg.Name, "module") - })) + storeRootPath := filepath.Join(projectPath, conf.Client.Vuex.Path, "generated") + options = append(options, + cosmosgen.WithVuexGeneration( + false, + func(m module.Module) string { + return filepath.Join(storeRootPath, giturl.UserAndRepo(m.Pkg.GoImportName), m.Pkg.Name, "module") + }, + storeRootPath, + ), + ) } return cosmosgen.Generate(context.Background(), projectPath, conf.Build.Proto.Path, options...) diff --git a/starport/templates/app/stargate/vue/src/App.vue b/starport/templates/app/stargate/vue/src/App.vue index 68a51077fa..31bb7a9700 100644 --- a/starport/templates/app/stargate/vue/src/App.vue +++ b/starport/templates/app/stargate/vue/src/App.vue @@ -3,33 +3,7 @@