Files
masonry/cmd/cli/commands.go

667 lines
18 KiB
Go

package main
import (
"bytes"
"embed"
_ "embed"
"fmt"
vue_gen "masonry/vue-gen"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"text/template"
"github.com/urfave/cli/v2"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"github.com/alecthomas/participle/v2"
"masonry/interpreter"
"masonry/lang"
)
//go:embed templates/proto/application.proto.tmpl
var protoTemplateSrc string
//go:embed templates/backend/main.go.tmpl
var mainGoTemplateSrc string
//go:embed templates/backend/gitignore.tmpl
var gitignoreTemplateSrc string
//go:embed proto_include/*
var protoInclude embed.FS
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.",
Category: "generator",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
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 openapi code
err = os.Mkdir("gen/openapi", 0755)
if err != nil {
return fmt.Errorf("error creating gen/openapi 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(mainGoTemplateSrc))
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 gitignore file
gitignoreFile, err := os.Create(".gitignore")
if err != nil {
return fmt.Errorf("error creating gitignore file | %w", err)
}
defer gitignoreFile.Close()
// render the gitignore file from the template
gitignoreTemplate := template.Must(template.New("gitignore").Parse(gitignoreTemplateSrc))
err = gitignoreTemplate.Execute(gitignoreFile, nil)
if err != nil {
return fmt.Errorf("error rendering gitignore 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(protoTemplateSrc))
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)
}
err = os.CopyFS("./", protoInclude)
if err != nil {
return fmt.Errorf("error copying proto include files | %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 or Masonry files",
Category: "generator",
Description: "This command will generate code from proto files or convert Masonry files to various formats.",
Subcommands: []*cli.Command{
{
Name: "proto",
Usage: "Generate code from proto files",
Action: func(c *cli.Context) error {
return generateProtoCode()
},
},
{
Name: "html",
Usage: "Generate HTML from Masonry files",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "input",
Usage: "Input Masonry file path",
Required: true,
Aliases: []string{"i"},
},
&cli.StringFlag{
Name: "output",
Usage: "Output directory for generated HTML files",
Value: "./output",
Aliases: []string{"o"},
},
},
Action: func(c *cli.Context) error {
inputFile := c.String("input")
outputDir := c.String("output")
return generateHTML(inputFile, outputDir)
},
},
},
Action: func(c *cli.Context) error {
// Default action - generate proto code for backward compatibility
return generateProtoCode()
},
}
}
// generateProtoCode handles the original proto code generation logic
func generateProtoCode() error {
fmt.Println("Generating code...")
protocArgs := []string{
"-I",
".",
"--go_out",
"gen/go",
"--go-grpc_out",
"gen/go",
"--go-grpc_opt=require_unimplemented_servers=false",
"--gorm_out",
"gen/go",
"--grpc-gateway_out",
"gen/go",
"--grpc-gateway_opt",
"logtostderr=true",
"--openapiv2_out",
"gen/openapi",
"--openapiv2_opt",
"logtostderr=true",
"--proto_path=./proto_include",
"proto/*.proto",
}
// generate go code
cmd := exec.Command(
"protoc",
protocArgs...,
)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
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 ts code
// if webapp folder is present, generate typescript code
if _, err := os.Stat("webapp"); err == nil {
err = os.Chdir("webapp")
if err != nil {
return fmt.Errorf("error changing directory to webapp | %w", err)
}
cmd = exec.Command("npx",
"openapi-typescript-codegen",
"--input",
"../gen/openapi/proto/service.swagger.json",
"--output",
"src/generated",
"--client",
"fetch",
)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
return fmt.Errorf("error generating typescript code | %w", err)
}
// make sure src/generated-sample-components exists
err = os.Mkdir("src/generated-sample-components", 0755)
if err != nil {
return fmt.Errorf("error creating src/generated-components directory | %w", err)
}
// generate vue components
err = vue_gen.GenVueFromSwagger("../gen/openapi/proto/service.swagger.json", "src/generated-sample-components")
if err != nil {
return fmt.Errorf("error generating vue components | %w", err)
}
cmd := exec.Command("npm", "install", "@masonitestudios/dynamic-vue")
err = cmd.Run()
if err != nil {
return fmt.Errorf("error installing @masonitestudios/dynamic-vue | %w", err)
}
err = os.Chdir("..")
if err != nil {
return fmt.Errorf("error changing directory back to root | %w", err)
}
}
return nil
}
func webappCmd() *cli.Command {
return &cli.Command{
Name: "webapp",
Aliases: []string{"w"},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
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 ",
Category: "generator",
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
},
}
}
func setupCmd() *cli.Command {
return &cli.Command{
Name: "setup",
Aliases: []string{"s"},
Usage: "Set up masonry, makes sure all dependencies are installed so Masonry can do its job.",
Action: func(c *cli.Context) error {
fmt.Printf("Running on %s/%s\n", runtime.GOOS, runtime.GOARCH)
if err := ensureDependencies(); err != nil {
return fmt.Errorf("error ensuring dependencies | %w", err)
}
return nil
},
}
}
func vueGenCmd() *cli.Command {
return &cli.Command{
Name: "vuegen",
Aliases: []string{"vg"},
Usage: "Generate vue components based on a swagger file",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "input",
Aliases: []string{"i"},
Usage: "The input swagger file",
Required: true,
},
&cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "The output directory",
Required: true,
},
},
Action: func(c *cli.Context) error {
input := c.String("input")
output := c.String("output")
fmt.Println("Generating typescript code")
cmd := exec.Command("npx",
"openapi-typescript-codegen",
"--input",
input,
"--output",
output,
"--client",
"fetch",
)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
return fmt.Errorf("error generating typescript code | %w", err)
}
fmt.Println("Generating vue components")
err = vue_gen.GenVueFromSwagger(input, fmt.Sprintf("%s/components/", output))
if err != nil {
return fmt.Errorf("error generating vue components | %w", err)
}
fmt.Println("You will need to run the command:\n\nnpm install @masonitestudios/dynamic-vue\n\nin your webapp's directory to use the generated components.")
return nil
},
}
}
// generateHTML parses a Masonry file and generates HTML output
func generateHTML(inputFile, outputDir string) error {
fmt.Printf("Generating HTML from %s to %s\n", inputFile, outputDir)
// Read the input file
content, err := os.ReadFile(inputFile)
if err != nil {
return fmt.Errorf("failed to read input file: %w", err)
}
// Create parser
parser, err := participle.Build[lang.AST]()
if err != nil {
return fmt.Errorf("failed to create parser: %w", err)
}
// Parse the Masonry file
ast, err := parser.ParseString(inputFile, string(content))
if err != nil {
return fmt.Errorf("failed to parse Masonry file: %w", err)
}
// Create HTML interpreter
htmlInterpreter := interpreter.NewHTMLInterpreter()
// Generate HTML
htmlFiles, err := htmlInterpreter.GenerateHTML(ast)
if err != nil {
return fmt.Errorf("failed to generate HTML: %w", err)
}
// Create output directory
err = os.MkdirAll(outputDir, 0755)
if err != nil {
return fmt.Errorf("failed to create output directory: %w", err)
}
// Write HTML files
for filename, content := range htmlFiles {
outputPath := filepath.Join(outputDir, filename)
err = os.WriteFile(outputPath, []byte(content), 0644)
if err != nil {
return fmt.Errorf("failed to write file %s: %w", outputPath, err)
}
fmt.Printf("Generated: %s\n", outputPath)
}
fmt.Printf("Successfully generated %d HTML file(s)\n", len(htmlFiles))
return nil
}
func serveCmd() *cli.Command {
return &cli.Command{
Name: "serve",
Usage: "Generate and run a simple HTTP server from a Masonry file",
Description: "This command parses a Masonry file and generates a simple Go HTTP server with in-memory database.",
Category: "development",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "file",
Usage: "Path to the Masonry file to interpret",
Required: true,
Aliases: []string{"f"},
},
&cli.StringFlag{
Name: "output",
Usage: "Output file for the generated server code",
Value: "server.go",
Aliases: []string{"o"},
},
&cli.BoolFlag{
Name: "run",
Usage: "Run the server after generating it",
Value: false,
Aliases: []string{"r"},
},
},
Action: func(c *cli.Context) error {
masonryFile := c.String("file")
outputFile := c.String("output")
shouldRun := c.Bool("run")
fmt.Printf("Parsing Masonry file: %s\n", masonryFile)
// Read the Masonry file
content, err := os.ReadFile(masonryFile)
if err != nil {
return fmt.Errorf("error reading Masonry file: %w", err)
}
// Parse the Masonry file
parser, err := participle.Build[lang.AST](
participle.Unquote("String"),
)
if err != nil {
return fmt.Errorf("error building parser: %w", err)
}
ast, err := parser.ParseString("", string(content))
if err != nil {
return fmt.Errorf("error parsing Masonry file: %w", err)
}
// Generate server code using the server interpreter
serverInterpreter := interpreter.NewServerInterpreter()
serverCode, err := serverInterpreter.Interpret(*ast)
if err != nil {
return fmt.Errorf("error interpreting Masonry file: %w", err)
}
// Write the generated server code to the output file
err = os.WriteFile(outputFile, []byte(serverCode), 0644)
if err != nil {
return fmt.Errorf("error writing server code to file: %w", err)
}
fmt.Printf("Server code generated successfully: %s\n", outputFile)
if shouldRun {
fmt.Println("Installing dependencies...")
// Initialize go module if it doesn't exist
if _, err := os.Stat("go.mod"); os.IsNotExist(err) {
cmd := exec.Command("go", "mod", "init", "masonry-server")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("error initializing go module: %w", err)
}
}
// Install required dependencies
dependencies := []string{
"github.com/google/uuid",
"github.com/gorilla/mux",
}
for _, dep := range dependencies {
fmt.Printf("Installing %s...\n", dep)
cmd := exec.Command("go", "get", dep)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("error installing dependency %s: %w", dep, err)
}
}
fmt.Printf("Running server from %s...\n", outputFile)
cmd := exec.Command("go", "run", outputFile)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
return nil
},
}
}
func templateCmd() *cli.Command {
return &cli.Command{
Name: "template",
Aliases: []string{"tmpl"},
Usage: "Generate code from templates using Masonry DSL",
Description: "This command parses a Masonry file and applies template files to generate code.",
Category: "generator",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "input",
Usage: "Path to the Masonry input file",
Required: true,
Aliases: []string{"i"},
},
&cli.StringFlag{
Name: "template",
Usage: "Path to a single template file",
Aliases: []string{"t"},
},
&cli.StringFlag{
Name: "template-dir",
Usage: "Path to a directory containing template files",
Aliases: []string{"d"},
},
&cli.StringFlag{
Name: "root-template",
Usage: "Name of the root template file when using template-dir (defaults to main.tmpl)",
Value: "main.tmpl",
Aliases: []string{"r"},
},
&cli.StringFlag{
Name: "output",
Usage: "Output file path (if not specified, prints to stdout)",
Aliases: []string{"o"},
},
},
Action: func(c *cli.Context) error {
inputFile := c.String("input")
templateFile := c.String("template")
templateDir := c.String("template-dir")
rootTemplate := c.String("root-template")
outputFile := c.String("output")
// Validate input parameters
if templateFile == "" && templateDir == "" {
return fmt.Errorf("either --template or --template-dir must be specified")
}
if templateFile != "" && templateDir != "" {
return fmt.Errorf("cannot specify both --template and --template-dir")
}
// Set default template directory and root template if none specified
if templateFile == "" && templateDir == "" {
templateDir = "./lang_templates"
rootTemplate = "basic_go_server.tmpl"
}
// Create template templateInterpreter
templateInterpreter := interpreter.NewTemplateInterpreter()
var result string
var err error
if templateFile != "" {
// Use single template file
result, err = templateInterpreter.InterpretFromFile(inputFile, templateFile)
} else {
// Use template directory
result, err = templateInterpreter.InterpretFromDirectory(inputFile, templateDir, rootTemplate)
}
if err != nil {
return fmt.Errorf("error generating code: %w", err)
}
// Output result
if outputFile != "" {
err = os.WriteFile(outputFile, []byte(result), 0644)
if err != nil {
return fmt.Errorf("error writing output file: %w", err)
}
fmt.Printf("Generated code written to: %s\n", outputFile)
} else {
fmt.Print(result)
}
return nil
},
}
}