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. :)
575 lines
15 KiB
Go
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
|
|
},
|
|
}
|
|
}
|