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 }, } } func generateMultiCmd() *cli.Command { return &cli.Command{ Name: "generate-multi", Aliases: []string{"gen-multi"}, Usage: "Generate multiple files using a template manifest", Description: "This command parses a Masonry file and applies a template manifest to generate multiple output files with per-item iteration support.", 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-dir", Usage: "Path to the directory containing template files", Required: true, Aliases: []string{"d"}, }, &cli.StringFlag{ Name: "manifest", Usage: "Name of the manifest file (defaults to manifest.yaml)", Value: "manifest.yaml", Aliases: []string{"m"}, }, &cli.StringFlag{ Name: "output-dir", Usage: "Output directory for generated files (defaults to ./generated)", Value: "./generated", Aliases: []string{"o"}, }, &cli.BoolFlag{ Name: "dry-run", Usage: "Show what files would be generated without writing them", Value: false, }, &cli.BoolFlag{ Name: "clean", Usage: "Clean output directory before generating files", Value: false, }, }, Action: func(c *cli.Context) error { inputFile := c.String("input") templateDir := c.String("template-dir") manifestFile := c.String("manifest") outputDir := c.String("output-dir") dryRun := c.Bool("dry-run") clean := c.Bool("clean") // Validate input files exist if _, err := os.Stat(inputFile); os.IsNotExist(err) { return fmt.Errorf("input file does not exist: %s", inputFile) } if _, err := os.Stat(templateDir); os.IsNotExist(err) { return fmt.Errorf("template directory does not exist: %s", templateDir) } manifestPath := filepath.Join(templateDir, manifestFile) if _, err := os.Stat(manifestPath); os.IsNotExist(err) { return fmt.Errorf("manifest file does not exist: %s", manifestPath) } fmt.Printf("Processing multi-file generation...\n") fmt.Printf("Input: %s\n", inputFile) fmt.Printf("Template Dir: %s\n", templateDir) fmt.Printf("Manifest: %s\n", manifestFile) fmt.Printf("Output Dir: %s\n", outputDir) // Clean output directory if requested if clean && !dryRun { if _, err := os.Stat(outputDir); !os.IsNotExist(err) { fmt.Printf("Cleaning output directory: %s\n", outputDir) err := os.RemoveAll(outputDir) if err != nil { return fmt.Errorf("error cleaning output directory: %w", err) } } } // Create template interpreter templateInterpreter := interpreter.NewTemplateInterpreter() // Generate multiple files using manifest outputs, err := templateInterpreter.InterpretToFiles(inputFile, templateDir, manifestFile) if err != nil { return fmt.Errorf("error generating files: %w", err) } if len(outputs) == 0 { fmt.Println("No files were generated (check your manifest conditions)") return nil } fmt.Printf("Generated %d file(s):\n", len(outputs)) if dryRun { // Dry run - just show what would be generated for filePath, content := range outputs { fmt.Printf(" [DRY-RUN] %s (%d bytes)\n", filePath, len(content)) } return nil } // Write all output files for filePath, content := range outputs { fullPath := filepath.Join(outputDir, filePath) // Create directory if it doesn't exist dir := filepath.Dir(fullPath) err := os.MkdirAll(dir, 0755) if err != nil { return fmt.Errorf("error creating directory %s: %w", dir, err) } // Write file err = os.WriteFile(fullPath, []byte(content), 0644) if err != nil { return fmt.Errorf("error writing file %s: %w", fullPath, err) } fmt.Printf(" Generated: %s\n", fullPath) } fmt.Printf("Successfully generated %d file(s) in %s\n", len(outputs), outputDir) return nil }, } }