diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index cfceb25..2af3cd3 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -7,7 +7,12 @@ import ( ) func main() { - commands := []*cli.Command{} + commands := []*cli.Command{ + createCmd(), + generateCmd(), + webappCmd(), + tailwindCmd(), + } app := &cli.App{ Name: "Masonry", diff --git a/cmd/cli/commands.go b/cmd/cli/commands.go index 06ab7d0..1c2b252 100644 --- a/cmd/cli/commands.go +++ b/cmd/cli/commands.go @@ -1 +1,206 @@ package main + +import ( + "bytes" + _ "embed" + "fmt" + "github.com/urfave/cli/v2" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "os" + "os/exec" + "strings" + "text/template" +) + +//go:embed templates/proto/application.proto.tmpl +var protoTemplate string + +//go:embed templates/backend/main.go.tmpl +var mainGoTemplate string + +func createCmd() *cli.Command { + return &cli.Command{ + Name: "create", + Aliases: []string{"c"}, + Usage: "Create a new app in a directory with the given name", + Description: "This command will create a new folder with the given name and generate a new app in that folder.", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Category: "generator", + Usage: "The name of the app to create", + Required: true, + Aliases: []string{"n"}, + }, + }, + Action: func(c *cli.Context) error { + applicationName := c.String("name") + + fmt.Printf("Creating app: %v\n", applicationName) + + // make a directory with the given name from the working directory + err := os.Mkdir(applicationName, 0755) + if err != nil { + return fmt.Errorf("error creating app directory | %w", err) + } + + // generate the app in the new directory + // cd into the new directory + err = os.Chdir(applicationName) + if err != nil { + return fmt.Errorf("error changing directory | %w", err) + } + + // initialize a go module + cmd := exec.Command("go", "mod", "init", applicationName) + err = cmd.Run() + if err != nil { + return fmt.Errorf("error initializing go module | %w", err) + } + + // create a directory to proto files + err = os.Mkdir("proto", 0755) + if err != nil { + return fmt.Errorf("error creating proto directory | %w", err) + } + + // create a directory to store the generated code + err = os.Mkdir("gen", 0755) + if err != nil { + return fmt.Errorf("error creating gen directory | %w", err) + } + + // create a directory for generated go code + err = os.Mkdir("gen/go", 0755) + if err != nil { + return fmt.Errorf("error creating gen/go directory | %w", err) + } + + // create a directory for generated typescript code + err = os.Mkdir("gen/ts", 0755) + if err != nil { + return fmt.Errorf("error creating gen/ts directory | %w", err) + } + + // create a main.go file + mainFile, err := os.Create("main.go") + if err != nil { + return fmt.Errorf("error creating main.go file | %w", err) + } + defer mainFile.Close() + + titleMaker := cases.Title(language.English) + + // render the main.go file from the template + goTemplate := template.Must(template.New("main").Parse(mainGoTemplate)) + err = goTemplate.Execute(mainFile, map[string]string{"AppName": strings.ToLower(applicationName), "AppNameCaps": titleMaker.String(applicationName)}) + if err != nil { + return fmt.Errorf("error rendering main.go file | %w", err) + } + + // create a proto file + protoFile, err := os.Create("proto/service.proto") + if err != nil { + return fmt.Errorf("error creating proto file | %w", err) + } + defer protoFile.Close() + + // render the proto file from the template + t := template.Must(template.New("proto").Parse(protoTemplate)) + err = t.Execute(protoFile, map[string]string{"AppName": strings.ToLower(applicationName), "AppNameCaps": titleMaker.String(applicationName), "ObjName": "Product"}) + if err != nil { + return fmt.Errorf("error rendering proto file | %w", err) + } + + // set up the webapp + err = setupWebapp("webapp") // since the app is already in its own named folder, we name it webapp + if err != nil { + return fmt.Errorf("error setting up webapp | %w", err) + } + + return nil + }, + } +} + +func generateCmd() *cli.Command { + return &cli.Command{ + Name: "generate", + Aliases: []string{"g"}, + Usage: "Generate code from proto files", + Description: "This command will generate code from the proto files in the proto directory and place them in a language folder in the gen folder.", + Action: func(c *cli.Context) error { + fmt.Println("Generating code...") + + // generate go code + cmd := exec.Command("protoc", "-I", ".", "--go_out", "gen/go", "--go-grpc_out", "gen/go", "--go-grpc_opt=require_unimplemented_servers=false", "--gorm_out", "gen/go", "proto/*.proto") + err := cmd.Run() + if err != nil { + + var buff bytes.Buffer + cmd.Stderr = &buff + fmt.Println(buff.String()) + + return fmt.Errorf("error generating go code | %w", err) + } + + //// generate typescript code + //cmd = exec.Command("protoc", "--ts_out=gen/ts", "proto/*.proto") + //err = cmd.Run() + //if err != nil { + // return fmt.Errorf("error generating typescript code | %w", err) + //} + + // TODO: if there is a webapp folder present, generate vue code from the proto files + + return nil + }, + } +} + +func webappCmd() *cli.Command { + return &cli.Command{ + Name: "webapp", + Aliases: []string{"w"}, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Category: "generator", + Usage: "The name of the webapp to create, this will be the name of the directory", + Required: true, + }, + }, + Usage: "Set up a webapp", + Description: "This command will set up a webapp ", + Action: func(c *cli.Context) error { + fmt.Println("Setting up webapp named: ", c.String("name")) + name := c.String("name") + + err := setupWebapp(name) + if err != nil { + return fmt.Errorf("error setting up webapp | %w", err) + } + + return nil + }, + } +} + +func tailwindCmd() *cli.Command { + return &cli.Command{ + Name: "tailwind", + Aliases: []string{"t"}, + Usage: "Set up tailwindcss in a vite webapp, if you built using masonry then this is already done for you", + Action: func(c *cli.Context) error { + fmt.Println("Setting up tailwindcss") + err := setupTailwind() + if err != nil { + return fmt.Errorf("error setting up tailwindcss | %w", err) + } + + return nil + }, + } +} diff --git a/cmd/cli/templates/backend/main.go.tmpl b/cmd/cli/templates/backend/main.go.tmpl new file mode 100644 index 0000000..3b958ef --- /dev/null +++ b/cmd/cli/templates/backend/main.go.tmpl @@ -0,0 +1,84 @@ +package main + +import ( + "context" + "github.com/payne8/go-libsql-dual-driver" + sqlite "github.com/ytsruh/gorm-libsql" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" + "gorm.io/gorm" + "log" + "net" + "os" + pb "{{ .AppName }}/gen/go" +) + +func main() { + logger := log.New(os.Stdout, "{{ .AppName }} ", log.LstdFlags) + primaryUrl := os.Getenv("LIBSQL_DATABASE_URL") + authToken := os.Getenv("LIBSQL_AUTH_TOKEN") + + tdb, err := libsqldb.NewLibSqlDB( + primaryUrl, + //libsqldb.WithMigrationFiles(migrationFiles), + libsqldb.WithAuthToken(authToken), + libsqldb.WithLocalDBName("local.db"), // will not be used for remote-only + ) + if err != nil { + logger.Printf("failed to open db %s: %s", primaryUrl, err) + log.Fatalln(err) + return + } + + // instantiate the grom ORM + gormDB, err := gorm.Open(sqlite.New(sqlite.Config{Conn: tdb.DB}), &gorm.Config{}) + if err != nil { + logger.Printf("failed to open gorm db %s: %s", primaryUrl, err) + log.Fatalln(err) + return + } + + // err = gormDB.AutoMigrate(&pb.UserORM{}) // TODO: figure out how to automate this part + // if err != nil { + // logger.Printf("failed to migrate user: %s", err) + // log.Fatalln(err) + // return + // } + // err = gormDB.AutoMigrate(&pb.ProductORM{}) // TODO: figure out how to automate this part + // if err != nil { + // logger.Printf("failed to migrate product: %s", err) + // log.Fatalln(err) + // return + // } + + handlers := pb.{{ .AppNameCaps }}DefaultServer{ + DB: gormDB, + } + + grpcServer := grpc.NewServer(grpc.ChainUnaryInterceptor( + func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) { + // TODO: this is an example of a middleware - we will use this for authentication + // we will have a function that checks the token and a switch statement that checks if the method is public or not + logger.Printf("request: %s", info.FullMethod) + return handler(ctx, req) + }, + )) + pb.Register{{ .AppNameCaps }}Server(grpcServer, &handlers) + reflection.Register(grpcServer) + + // start the server + listener, err := net.Listen("tcp", ":9000") + if err != nil { + logger.Printf("failed to listen: %s", err) + log.Fatalln(err) + return + } + logger.Printf("listening on %s", listener.Addr().String()) + + err = grpcServer.Serve(listener) + if err != nil { + logger.Printf("failed to serve: %s", err) + log.Fatalln(err) + return + } +} diff --git a/templates/cli/cli.go b/cmd/cli/templates/cli/cli.go similarity index 100% rename from templates/cli/cli.go rename to cmd/cli/templates/cli/cli.go diff --git a/templates/cli/commands.go b/cmd/cli/templates/cli/commands.go similarity index 100% rename from templates/cli/commands.go rename to cmd/cli/templates/cli/commands.go diff --git a/cmd/cli/templates/proto/application.proto.tmpl b/cmd/cli/templates/proto/application.proto.tmpl new file mode 100644 index 0000000..17532ce --- /dev/null +++ b/cmd/cli/templates/proto/application.proto.tmpl @@ -0,0 +1,68 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +syntax = "proto3"; + +package {{ .AppName }}; + +import "gorm/options/gorm.proto"; +//import "gorm/types/types.proto"; + +option go_package = "./;pb"; + +service {{ .AppNameCaps }} { + option (gorm.server).autogen = true; + // Add your service methods here + + rpc Create{{ .ObjName }} (Create{{ .ObjName }}Request) returns (Create{{ .ObjName }}Response) {} + + rpc Read{{ .ObjName }} (Read{{ .ObjName }}Request) returns (Read{{ .ObjName }}Response) {} + + rpc List{{ .ObjName }}s (List{{ .ObjName }}sRequest) returns (List{{ .ObjName }}sResponse) {} + + rpc Update{{ .ObjName }} (Update{{ .ObjName }}Request) returns (Update{{ .ObjName }}Response) {} + + rpc Delete{{ .ObjName }} (Delete{{ .ObjName }}Request) returns (Delete{{ .ObjName }}Response) { + option (gorm.method).object_type = "{{ .ObjName }}"; + } +} + +message Create{{ .ObjName }}Request { + {{ .ObjName }} payload = 1; +} + +message Create{{ .ObjName }}Response { + {{ .ObjName }} result = 1; +} + +message Read{{ .ObjName }}Request { + uint64 id = 1; +} + +message Read{{ .ObjName }}Response { + {{ .ObjName }} result = 1; +} + +message List{{ .ObjName }}sRequest {} + +message List{{ .ObjName }}sResponse { + repeated {{ .ObjName }} results = 1; +} + +message Update{{ .ObjName }}Request { + {{ .ObjName }} payload = 1; +} + +message Update{{ .ObjName }}Response { + {{ .ObjName }} result = 1; +} + +message Delete{{ .ObjName }}Request { + uint64 id = 1; +} + +message Delete{{ .ObjName }}Response {} + +message {{ .ObjName }} { + option (gorm.opts).ormable = true; + uint64 id = 1; + // add object fields here +} diff --git a/cmd/cli/templates/vue/AboutView.vue.tmpl b/cmd/cli/templates/vue/AboutView.vue.tmpl new file mode 100644 index 0000000..b466f70 --- /dev/null +++ b/cmd/cli/templates/vue/AboutView.vue.tmpl @@ -0,0 +1,15 @@ + + + diff --git a/cmd/cli/templates/vue/HomeView.vue.tmpl b/cmd/cli/templates/vue/HomeView.vue.tmpl new file mode 100644 index 0000000..155409b --- /dev/null +++ b/cmd/cli/templates/vue/HomeView.vue.tmpl @@ -0,0 +1,8 @@ + + + diff --git a/cmd/cli/webapp.go b/cmd/cli/webapp.go new file mode 100644 index 0000000..981b5c8 --- /dev/null +++ b/cmd/cli/webapp.go @@ -0,0 +1,214 @@ +package main + +import ( + _ "embed" + "fmt" + "os" + "os/exec" + "strings" +) + +//go:embed templates/vue/AboutView.vue.tmpl +var aboutViewTemplate string + +//go:embed templates/vue/HomeView.vue.tmpl +var homeViewTemplate string + +func setupWebapp(name string) error { + fmt.Println("Setting up webapp with Vue 3, TypeScript, Vue Router, Pinia, and ESLint with Prettier") + cmd := exec.Command("npm", "create", "vue@latest", "--", "--ts", "--router", "--pinia", "--eslint-with-prettier", name) + err := cmd.Run() + if err != nil { + return fmt.Errorf("error setting up webapp | %w", err) + } + + fmt.Println("Switching to the new directory") + // cd into the new directory + err = os.Chdir(name) + if err != nil { + return fmt.Errorf("error changing directory | %w", err) + } + + fmt.Println("Installing base dependencies") + // install all dependencies and format the code + cmd = exec.Command("npm", "install", "&&", "npm", "run", "format") + err = cmd.Run() + if err != nil { + return fmt.Errorf("error changing directory | %w", err) + } + + // make src/assets/base.css and src/assets/main.css mostly empty files + err = os.WriteFile("src/assets/base.css", []byte(""), 0644) + if err != nil { + return fmt.Errorf("error writing base.css | %w", err) + } + err = os.WriteFile("src/assets/main.css", []byte("@import './base.css';"), 0644) + if err != nil { + return fmt.Errorf("error writing main.css | %w", err) + } + + fmt.Println("Setting up Tailwind CSS") + err = setupTailwind() + if err != nil { + return fmt.Errorf("error setting up tailwind | %w", err) + } + + // clean up the default example files + err = removeListOfFiles([]string{ + "src/components/HelloWorld.vue", + "src/components/TheWelcome.vue", + "src/components/WelcomeItem.vue", + "src/views/AboutView.vue", // replace with new AboutView.vue + "src/components/icons/IconCommunity.vue", + "src/components/icons/IconDocumentation.vue", + "src/components/icons/IconEcosystem.vue", + "src/components/icons/IconSupport.vue", + "src/components/icons/IconTooling.vue", + "src/assets/logo.svg", + }) + + // create a new about view from the aboutViewTemplate + err = os.WriteFile("src/views/AboutView.vue", []byte(aboutViewTemplate), 0644) + if err != nil { + return fmt.Errorf("error writing new AboutView.vue | %w", err) + } + // create a new home view from the homeViewTemplate + err = os.WriteFile("src/views/HomeView.vue", []byte(homeViewTemplate), 0644) + if err != nil { + return fmt.Errorf("error writing new HomeView.vue | %w", err) + } + + fmt.Println("Exiting the new directory") + err = os.Chdir("..") + if err != nil { + return fmt.Errorf("error changing directory | %w", err) + } + + return nil +} + +func setupTailwind() error { + // check that there is a vite.config.ts file and a package.json file + _, err := os.Stat("vite.config.ts") + if err != nil { + return fmt.Errorf("error checking for vite.config.ts | %w", err) + } + _, err = os.Stat("package.json") + if err != nil { + return fmt.Errorf("error checking for package.json | %w", err) + } + + cmd := exec.Command("npm", "install", "tailwindcss", "@tailwindcss/vite") + err = cmd.Run() + if err != nil { + return fmt.Errorf("error installing tailwindcss | %w", err) + } + + // In the vite.config.ts insert the following code after the last import statement `import tailwindcss from '@tailwindcss/vite'` + err = insertImportStatement("import tailwindcss from '@tailwindcss/vite'", "vite.config.ts") + if err != nil { + return fmt.Errorf("error inserting import statement | %w", err) + } + + // In the vite.config.ts insert the following code after the last plugin usage `tailwindcss(),` + err = insertPluginUsage(" tailwindcss(),", "vite.config.ts") + if err != nil { + return fmt.Errorf("error inserting plugin usage | %w", err) + } + + // in the base.css file add an @import statement for tailwind + // check that './src/assets/base.css' exists + _, err = os.Stat("./src/assets/base.css") + if err != nil { + return fmt.Errorf("error checking for base.css | %w", err) + } + err = insertAtTopLineOfFile("@import \"tailwindcss\";", "./src/assets/base.css") + if err != nil { + return fmt.Errorf("error inserting import statement | %w", err) + } + return nil +} + +func insertImportStatement(importStatement string, filename string) error { + fileContent, err := os.ReadFile(fmt.Sprintf(".%c%s", os.PathSeparator, filename)) + if err != nil { + return fmt.Errorf("error reading %s | %w", filename, err) + } + + fileLines := strings.Split(strings.ReplaceAll(string(fileContent), "\r\n", "\n"), "\n") + lastImportIndex := -1 + // finds the last import statement in the file and appends after that one + for i, line := range fileLines { + if strings.HasPrefix(line, "import") { + lastImportIndex = i + } + } + if lastImportIndex != -1 { + fileLines = append(fileLines[:lastImportIndex+1], append([]string{importStatement}, fileLines[lastImportIndex+1:]...)...) + } else { + // add the import to the top of the file + fileLines = append([]string{importStatement}, fileLines...) + } + + err = os.WriteFile(filename, []byte(strings.Join(fileLines, "\n")), 0644) + if err != nil { + return fmt.Errorf("error writing vite.config.ts | %w", err) + } + return nil +} + +func insertPluginUsage(pluginUsage string, filename string) error { + fileContent, err := os.ReadFile(fmt.Sprintf(".%c%s", os.PathSeparator, filename)) + if err != nil { + return fmt.Errorf("error reading %s | %w", filename, err) + } + + fileLines := strings.Split(strings.ReplaceAll(string(fileContent), "\r\n", "\n"), "\n") + startOfPluginsIndex := -1 + // finds the `plugins: [` line in the file and appends after that one + for i, line := range fileLines { + if strings.Contains(line, "plugins: [") { + startOfPluginsIndex = i + } + } + if startOfPluginsIndex != -1 { + fileLines = append(fileLines[:startOfPluginsIndex+1], append([]string{pluginUsage}, fileLines[startOfPluginsIndex+1:]...)...) + } else { + // add the import to the top of the file + fileLines = append([]string{pluginUsage}, fileLines...) + } + + err = os.WriteFile(filename, []byte(strings.Join(fileLines, "\n")), 0644) + if err != nil { + return fmt.Errorf("error writing vite.config.ts | %w", err) + } + return nil +} + +func insertAtTopLineOfFile(content string, filename string) error { + fileContent, err := os.ReadFile(fmt.Sprintf(".%c%s", os.PathSeparator, filename)) + if err != nil { + return fmt.Errorf("error reading %s | %w", filename, err) + } + + fileLines := strings.Split(strings.ReplaceAll(string(fileContent), "\r\n", "\n"), "\n") + fileLines = append([]string{content}, fileLines...) + + err = os.WriteFile(filename, []byte(strings.Join(fileLines, "\n")), 0644) + if err != nil { + return fmt.Errorf("error writing vite.config.ts | %w", err) + } + return nil +} + +func removeListOfFiles(files []string) error { + + for _, file := range files { + err := os.Remove(file) + if err != nil { + return fmt.Errorf("error removing %s | %w", file, err) + } + } + + return nil +} diff --git a/go.mod b/go.mod index 5e59770..130a2e4 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module masonry go 1.23 -require github.com/urfave/cli/v2 v2.27.5 +require ( + github.com/urfave/cli/v2 v2.27.5 + golang.org/x/text v0.22.0 +) require ( github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect diff --git a/go.sum b/go.sum index c8b6c7e..3f981b0 100644 --- a/go.sum +++ b/go.sum @@ -6,3 +6,5 @@ github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= diff --git a/templates/backend/daemon/daemon.go b/templates/backend/daemon/daemon.go deleted file mode 100644 index 189e9b1..0000000 --- a/templates/backend/daemon/daemon.go +++ /dev/null @@ -1,26 +0,0 @@ -package daemon - -type DaemonService interface { - Start() - Stop() -} - -type Daemon struct { - services []DaemonService -} - -func (d *Daemon) RegisterDaemonServer(service DaemonService) { - d.services = append(d.services, service) -} - -func (d *Daemon) Start() { - for _, service := range d.services { - go service.Start() - } -} - -func (d *Daemon) Stop() { - for _, service := range d.services { - service.Stop() - } -} diff --git a/templates/backend/daemon/readme.md b/templates/backend/daemon/readme.md deleted file mode 100644 index 27e94d9..0000000 --- a/templates/backend/daemon/readme.md +++ /dev/null @@ -1,3 +0,0 @@ -# Daemon - -A library that provides a way to register and start multiple long-running services with safe tear-down when they are done. \ No newline at end of file diff --git a/templates/backend/main.go b/templates/backend/main.go deleted file mode 100644 index fe7f767..0000000 --- a/templates/backend/main.go +++ /dev/null @@ -1,5 +0,0 @@ -package main - -func main() { - -}