Files
masonry/cmd/cli/commands.go
Mason Payne d36e1bfd86 add html and server interpreters
these are basic and lack most features.
the server seems to work the best.
the html on the other hand is really rough and doesn't seem to work yet.
but it does build the pages and they have all the shapes and sections we
wanted. More work to come. :)
2025-08-25 00:50:55 -06:00

575 lines
15 KiB
Go

package main
import (
"bytes"
"embed"
_ "embed"
"fmt"
"github.com/urfave/cli/v2"
"golang.org/x/text/cases"
"golang.org/x/text/language"
vue_gen "masonry/vue-gen"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"text/template"
"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
},
}
}