diff --git a/cli/coffee/build.go b/cli/coffee/build.go index 59388fa..9b5ddb5 100644 --- a/cli/coffee/build.go +++ b/cli/coffee/build.go @@ -1,7 +1,6 @@ package main import ( - "errors" "github.com/YairLevi/Coffee/cli/coffee/util" "github.com/charmbracelet/log" "os" @@ -12,41 +11,67 @@ var sourceDirMapping = map[string]string{ "angular-ts": "frontend/dist/angular-ts/browser", } -func Build() error { +func Build() { if len(os.Args) < 3 { - return errors.New("specify which ui template to build for (will change later)") + log.Error("specify which ui template to build for (will change later)") + return } err := os.Chdir("frontend") if err != nil { - return err + log.Errorf("error: %v", err) + return } - log.Info("Making frontend dist folder") - buildFront := util.CommandWithLog("npm", "run", "build") - err = buildFront.Run() + _, err = RunCommand(CmdProps{ + Cmd: BuildFrontend, + Sync: true, + LogBefore: "Making frontend dist folder", + Opts: Opts(WithStdout, WithStderr), + }) if err != nil { - return err + log.Errorf("Failed to build frontend: %v", err) + return } err = os.Chdir("..") if err != nil { - return err + log.Errorf("error moving directory: %v", err) + return } log.Info("Adding frontend to resources") sourceDir := sourceDirMapping[os.Args[2]] err = util.MoveDirectory(sourceDir, "src/main/resources/dist") if err != nil { - return err + log.Errorf("Failed to move dist folder to the resources folder. %v", err) + return } - log.Info("Bundling to JAR") - buildApp := util.CommandWithLog("mvn", "clean", "compile", "assembly:single") - err = buildApp.Run() + log.Info("Preparing for bundle") + file, err := os.Create("src/main/resources/__jar__") if err != nil { - return err + log.Errorf("Unexpected error: production flag was not able to set. %v", err) + return + } + file.Close() + + _, err = RunCommand(CmdProps{ + Cmd: BundleApp, + LogBefore: "Bundling to JAR", + Sync: true, + Opts: Opts(WithStdout, WithStderr), + }) + if err != nil { + log.Errorf("Failed to build app into JAR. %v", err) + return + } + + log.Info("Post bundle cleanup") + err = os.Remove("src/main/resources/__jar__") + if err != nil { + log.Errorf("Unexpected error: was not able to delete temporary production flag. %v", err) + return } log.Info("Done. your JAR is located at `./target") - return nil } diff --git a/cli/coffee/commands.go b/cli/coffee/commands.go new file mode 100644 index 0000000..00be944 --- /dev/null +++ b/cli/coffee/commands.go @@ -0,0 +1,61 @@ +package main + +import ( + "github.com/charmbracelet/log" + "os" + "os/exec" + "strings" +) + +const ( + InstallFrontendDependencies = "npm install" + LaunchDevServer = "npm run dev" + BuildFrontend = "npm run build" + CompileBackend = "mvn compile" + BundleApp = "mvn compile assembly:single" + LaunchApp = "mvn exec:java" + GenerateAppBinds = "mvn exec:java -Dexec.args=\"generate\"" +) + +type Opt = func(cmd *exec.Cmd) + +func WithStdout(cmd *exec.Cmd) { + cmd.Stdout = os.Stdout +} + +func WithStderr(cmd *exec.Cmd) { + cmd.Stderr = os.Stderr +} + +type CmdProps struct { + Cmd string + LogBefore string + Sync bool + Opts []Opt +} + +func Opts(opts ...Opt) []Opt { + var ropts []Opt + for _, o := range opts { + ropts = append(ropts, o) + } + return ropts +} + +func RunCommand(props CmdProps) (*exec.Cmd, error) { + words := strings.Split(props.Cmd, " ") + name := words[0] + args := words[1:] + command := exec.Command(name, args...) + for _, opt := range props.Opts { + opt(command) + } + var err error = nil + log.Info(props.LogBefore) + if props.Sync { + err = command.Run() + } else { + err = command.Start() + } + return command, err +} diff --git a/cli/coffee/dev.go b/cli/coffee/dev.go index 43b8042..f2e32e8 100644 --- a/cli/coffee/dev.go +++ b/cli/coffee/dev.go @@ -4,63 +4,94 @@ import ( "fmt" "github.com/YairLevi/Coffee/cli/coffee/util" "github.com/charmbracelet/log" + "net" "os" - "os/exec" + "time" ) -func Dev() error { - err := os.Chdir("frontend") +func Dev() { + _, err := RunCommand(CmdProps{ + Cmd: CompileBackend, + Sync: true, + Opts: Opts(WithStderr, WithStdout), + LogBefore: "Compiling Application", + }) if err != nil { - fmt.Println(err.Error()) - return err + log.Error("Failed to compile backend code.", "err", err) + return } - log.Info("Installing dependencies") - err = exec.Command("npm", "install").Run() + + _, err = RunCommand(CmdProps{ + Cmd: GenerateAppBinds, + LogBefore: "Generating type-safe frontend bindings...", + Sync: true, + Opts: Opts(WithStderr), + }) if err != nil { - fmt.Println(err.Error()) - return err + log.Errorf("Failed to create bindings. %v", err) + return } - log.Info("Starting development server") - devServer := exec.Command("npm", "run", "dev") - devServer.Stderr = os.Stderr - err = devServer.Start() + err = os.Chdir("frontend") if err != nil { - log.Error(err.Error()) - return err + log.Error("Can't go to frontend directory.", "err", err) + return } - err = os.Chdir("..") + _, err = RunCommand(CmdProps{ + Cmd: InstallFrontendDependencies, + LogBefore: "Installing dependencies", + Sync: true, + }) if err != nil { - fmt.Println(err.Error()) - return err + log.Error("Failed to download NPM dependencies.", "err", err) + return } - log.Info("Compiling Application") - compile := exec.Command("mvn", "clean", "compile") - compile.Stderr = os.Stderr - err = compile.Run() + devServerCmd, err := RunCommand(CmdProps{ + Cmd: LaunchDevServer, + LogBefore: "Starting development server", + Opts: Opts(WithStderr), + Sync: false, + }) if err != nil { - log.Error(err.Error()) - return err + log.Error("Failed to start dev server.", "err", err) + return + } + defer func() { + log.Info("Shutting down frontend dev server") + err = util.StopProcessTree(devServerCmd.Process.Pid) + if err != nil { + log.Error("Failed to close the entire process tree of the dev server.", "err", err) + } + }() + log.Info("Waiting for dev server to fully start") + for { + host, port := "localhost", "5173" + timeout := time.Millisecond * 500 + conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), timeout) + if err == nil { + break + } + if conn != nil { + conn.Close() + } } - log.Info("Running. application logging below") - log.Info("\n==================================\n") - cmd := exec.Command("mvn", "exec:java") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err = cmd.Run() + err = os.Chdir("..") if err != nil { - fmt.Println(err.Error()) - return err + log.Error("Can't go back to project directory.", "err", err) + return } - log.Info("\n==================================\n") - log.Info("Shutting down frontend dev server") - err = util.StopProcessTree(devServer.Process.Pid) + + _, err = RunCommand(CmdProps{ + Cmd: LaunchApp, + Opts: Opts(WithStderr, WithStdout), + Sync: true, + LogBefore: "Running. application logging below", + }) if err != nil { - log.Error("Failed to close the entire process tree of the dev server.") - return err + fmt.Println("Failed to launch application.", "err", err) + return } - return nil } diff --git a/cli/coffee/generate.go b/cli/coffee/generate.go new file mode 100644 index 0000000..1e81fbc --- /dev/null +++ b/cli/coffee/generate.go @@ -0,0 +1,27 @@ +package main + +import "github.com/charmbracelet/log" + +func Generate() { + _, err := RunCommand(CmdProps{ + Cmd: CompileBackend, + Sync: true, + LogBefore: "Compiling backend code", + Opts: Opts(WithStdout, WithStderr), + }) + if err != nil { + log.Errorf("Error compiling backend code for generate. %v", err) + return + } + + _, err = RunCommand(CmdProps{ + Cmd: GenerateAppBinds, + Opts: Opts(WithStdout, WithStderr), + LogBefore: "Generating bindings.", + Sync: true, + }) + if err != nil { + log.Errorf("Couldn't generate bindings for some reason. %v", err) + return + } +} diff --git a/cli/coffee/init.go b/cli/coffee/init.go index 585913f..4c000ba 100644 --- a/cli/coffee/init.go +++ b/cli/coffee/init.go @@ -1,9 +1,7 @@ package main import ( - "errors" "fmt" - "github.com/YairLevi/Coffee/cli/coffee/util" "github.com/charmbracelet/log" "io" "io/fs" @@ -12,38 +10,12 @@ import ( "strings" ) -func printAvailableTemplates() { - uiTemplatePath := "templates/ui" - entries, err := fs.ReadDir(content, uiTemplatePath) - if err != nil { - log.Error("unexpected internal error.", "err", err) - os.Exit(1) - } - log.Info("Available frontend templates:") - for _, entry := range entries { - log.Info("\t" + entry.Name()) - } - - backendTemplatePath := "templates/backend" - entries, err = fs.ReadDir(content, backendTemplatePath) - if err != nil { - log.Error("unexpected internal error.", "err", err) - os.Exit(1) - } - log.Info("Available backend templates:") - for _, entry := range entries { - log.Info("\t" + entry.Name()) - } -} - -func Init() error { +func Init() { if len(os.Args) < 4 { - return util.LogAndReturn( - log.Error, - "not enough arguments.\nproper usage is: coffee ", - errors.New("usage error"), - ) + log.Error("not enough arguments.\nproper usage is: coffee init ") + return } + var ( uiTemplatePath = "templates/ui/" backendTemplatePath = "templates/backend/" @@ -58,31 +30,21 @@ func Init() error { backExists, err := SubdirectoryExists(backendTemplatePath + backend) uiExists, err := SubdirectoryExists(uiTemplatePath + ui) if err != nil { - return util.LogAndReturn( - log.Error, - fmt.Sprint("unexpected cli error"), - errors.New("internal error"), - ) + log.Error("unexpected cli error") + return } if !backExists { - err := util.LogAndReturn( - log.Error, - fmt.Sprint("backend template ", backend, " does not exist. check the valid templates."), - errors.New("invalid backend template error"), - ) - printAvailableTemplates() - return err + log.Error("backend template ", backend, " does not exist. check the valid templates.") + PrintAvailableTemplates() + return } if !uiExists { - err := util.LogAndReturn( - log.Error, - fmt.Sprint("frontend template ", ui, " does not exist. check the valid templates."), - errors.New("invalid frontend template error"), - ) - printAvailableTemplates() - return err + log.Error("frontend template ", ui, " does not exist. check the valid templates.") + PrintAvailableTemplates() + return } + err = os.Mkdir(baseProjectDirUI, 0666) if err != nil { panic(err) @@ -91,20 +53,22 @@ func Init() error { log.Info("Creating backend template files for " + backend) err = CopyFiles(backendTemplatePath+backend, baseProjectDir) if err != nil { - return fmt.Errorf("creating backend files error: %v", err.Error()) + log.Errorf("creating backend files error: %v", err.Error()) + return } log.Info("Creating frontend template files for " + ui) err = CopyFiles(uiTemplatePath+ui, baseProjectDirUI) if err != nil { - return fmt.Errorf("creating frontend files error: %v", err.Error()) + log.Errorf("creating frontend files error: %v", err.Error()) + return } log.Info("Doing some internal work...") err = ResetPrefixedFiles(".") if err != nil { - return fmt.Errorf("failed to rename file: %v", err) + log.Errorf("failed to rename file: %v", err) + return } log.Info("Done! your project is created.") - return nil } func CopyFiles(src, dest string) error { @@ -164,7 +128,7 @@ func SubdirectoryExists(subDir string) (bool, error) { entries, err := fs.ReadDir(content, subDir) if err != nil { if os.IsNotExist(err) { - return false, nil // Subdirectory does not exist + return false, fmt.Errorf("directory doesn't exist") // Subdirectory does not exist } return false, fmt.Errorf("error reading directory: %v", err) } @@ -201,3 +165,27 @@ func ResetPrefixedFiles(tempPath string) error { return nil }) } + +func PrintAvailableTemplates() { + uiTemplatePath := "templates/ui" + entries, err := fs.ReadDir(content, uiTemplatePath) + if err != nil { + log.Error("unexpected internal error.", "err", err) + os.Exit(1) + } + log.Info("Available frontend templates:") + for _, entry := range entries { + log.Info("\t" + entry.Name()) + } + + backendTemplatePath := "templates/backend" + entries, err = fs.ReadDir(content, backendTemplatePath) + if err != nil { + log.Error("unexpected internal error.", "err", err) + os.Exit(1) + } + log.Info("Available backend templates:") + for _, entry := range entries { + log.Info("\t" + entry.Name()) + } +} diff --git a/cli/coffee/main.go b/cli/coffee/main.go index c30706f..3af3ee2 100644 --- a/cli/coffee/main.go +++ b/cli/coffee/main.go @@ -3,35 +3,32 @@ package main import ( "embed" "fmt" - "github.com/charmbracelet/log" "os" ) //go:embed templates/* var content embed.FS -func perform(f func() error) { - if err := f(); err != nil { - log.Error(err.Error()) - os.Exit(1) - } -} - -const INIT = "init" -const DEV = "dev" -const BUILD = "build" +const ( + INIT = "init" + DEV = "dev" + BUILD = "build" + GENERATE = "generate" +) func main() { cmd := os.Args[1] switch cmd { case INIT: - perform(Init) + Init() + case GENERATE: + Generate() case DEV: - perform(Dev) + Dev() case BUILD: - perform(Build) + Build() default: - fmt.Println("invalid usage. proper usage is: coffee ") + fmt.Println("invalid usage. undefined command " + cmd) return // TODO: add option to print out all available templates. } diff --git a/cli/coffee/templates/backend/java/pom.xml b/cli/coffee/templates/backend/java/pom.xml index 8d390b3..0b3871e 100644 --- a/cli/coffee/templates/backend/java/pom.xml +++ b/cli/coffee/templates/backend/java/pom.xml @@ -14,6 +14,25 @@ UTF-8 + + + mavenCentral + https://repo1.maven.org/maven2/ + + + jitpack.io + https://jitpack.io + + + + + + com.github.YairLevi + Coffee + 0.2.1-test-7 + + + src/main/java src/test/java @@ -35,7 +54,7 @@ - MainKt + Main @@ -48,17 +67,9 @@ exec-maven-plugin 1.6.0 - MainKt + Main - - - - com.github.YairLevi - Coffee - 0.1.9 - - \ No newline at end of file diff --git a/cli/coffee/templates/backend/java/src/main/java/Main.java b/cli/coffee/templates/backend/java/src/main/java/Main.java index 74cb214..1d69dd1 100644 --- a/cli/coffee/templates/backend/java/src/main/java/Main.java +++ b/cli/coffee/templates/backend/java/src/main/java/Main.java @@ -3,9 +3,10 @@ public class Main { public static void main(String[] args) { - Window w = new Window(true); + Window w = new Window(args); w.setSize(800, 600); w.setTitle("Java coffee app!"); + w.setURL("http://localhost:5173"); w.bind(new App()); w.run(); } diff --git a/cli/coffee/templates/backend/kotlin/pom.xml b/cli/coffee/templates/backend/kotlin/pom.xml index 544bbbd..9da9894 100644 --- a/cli/coffee/templates/backend/kotlin/pom.xml +++ b/cli/coffee/templates/backend/kotlin/pom.xml @@ -29,6 +29,31 @@ + + + com.github.YairLevi + Coffee + 0.2.1-test-7 + + + org.jetbrains.kotlin + kotlin-test-junit5 + 1.9.21 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.10.0 + test + + + org.jetbrains.kotlin + kotlin-stdlib + 1.9.21 + + + src/main/kotlin src/test/kotlin @@ -99,29 +124,4 @@ - - - com.github.YairLevi - Coffee - 0.1.9 - - - org.jetbrains.kotlin - kotlin-test-junit5 - 1.9.21 - test - - - org.junit.jupiter - junit-jupiter-engine - 5.10.0 - test - - - org.jetbrains.kotlin - kotlin-stdlib - 1.9.21 - - - \ No newline at end of file diff --git a/cli/coffee/templates/backend/kotlin/src/main/kotlin/Main.kt b/cli/coffee/templates/backend/kotlin/src/main/kotlin/Main.kt index c28247f..d3bef86 100644 --- a/cli/coffee/templates/backend/kotlin/src/main/kotlin/Main.kt +++ b/cli/coffee/templates/backend/kotlin/src/main/kotlin/Main.kt @@ -1,7 +1,7 @@ import org.levi.coffee.Window -fun main() { - val w = Window(dev = true) +fun main(args: Array) { + val w = Window(args) w.setSize(800, 600) w.setURL("http://localhost:5173") w.setTitle("Kotlin coffee app!") diff --git a/cli/coffee/templates/ui/angular-ts/angular.json b/cli/coffee/templates/ui/angular-ts/angular.json index 2f6e669..0c96904 100644 --- a/cli/coffee/templates/ui/angular-ts/angular.json +++ b/cli/coffee/templates/ui/angular-ts/angular.json @@ -17,7 +17,7 @@ "build": { "builder": "@angular-devkit/build-angular:application", "options": { - "outputPath": "dist/my-app", + "outputPath": "dist/angular-ts", "index": "src/index.html", "browser": "src/main.ts", "polyfills": [ diff --git a/src/main/kotlin/org/levi/coffee/Window.kt b/src/main/kotlin/org/levi/coffee/Window.kt index 64be34c..e040446 100644 --- a/src/main/kotlin/org/levi/coffee/Window.kt +++ b/src/main/kotlin/org/levi/coffee/Window.kt @@ -11,25 +11,22 @@ import org.levi.coffee.internal.MethodBinder import org.levi.coffee.internal.util.FileUtil import org.slf4j.Logger import org.slf4j.LoggerFactory +import java.net.ServerSocket import java.util.* import java.util.function.Consumer import kotlin.system.exitProcess -class Window(val dev: Boolean = true) { +class Window(val args: Array) { private val log: Logger = LoggerFactory.getLogger(this::class.java) - - private val _webview: Webview = Webview(true) private val _beforeStartCallbacks: MutableList = ArrayList() private val _onCloseCallbacks: MutableList = ArrayList() private val _bindObjects = ArrayList() - private var _url: String = "" + private val _webviewInitFunctions: MutableList<(wv: Webview) -> Unit> = ArrayList() - init { - setSize(800, 600) - } + private val dev = Thread.currentThread().contextClassLoader.getResource("__jar__") == null fun setURL(url: String) { - _url = url + _webviewInitFunctions.add { it.loadURL(url) } } fun setHTMLFromResource(resourcePath: String) { @@ -38,7 +35,7 @@ class Window(val dev: Boolean = true) { log.error("Resource at $resourcePath was not found.") exitProcess(1) } - _url = resource.toURI().toString() + this.setURL(resource.toURI().toString()) } fun setRawHTMLFromFile(path: String, isBase64: Boolean = false) { @@ -48,32 +45,33 @@ class Window(val dev: Boolean = true) { } fun setRawHTML(html: String, isBase64: Boolean = false) { - _url = "data:text/html" + var url = "data:text/html" if (isBase64) { - _url += ";base64,${Base64.getEncoder().encodeToString(html.toByteArray())}" + url += ";base64,${Base64.getEncoder().encodeToString(html.toByteArray())}" } else { - _url += ",$html" + url += ",$html" } + this.setURL(url) } fun setTitle(title: String) { - _webview.setTitle(title) + _webviewInitFunctions.add { it.setTitle(title) } } fun setSize(width: Int, height: Int) { - _webview.setSize(width, height) + _webviewInitFunctions.add { it.setSize(width, height) } } fun setMinSize(minWidth: Int, minHeight: Int) { - _webview.setMinSize(minWidth, minHeight) + _webviewInitFunctions.add { it.setMinSize(minWidth, minHeight) } } fun setMaxSize(maxWidth: Int, maxHeight: Int) { - _webview.setMaxSize(maxWidth, maxHeight) + _webviewInitFunctions.add { it.setMaxSize(maxWidth, maxHeight) } } fun setFixedSize(fixedWidth: Int, fixedHeight: Int) { - _webview.setFixedSize(fixedWidth, fixedHeight) + _webviewInitFunctions.add { it.setFixedSize(fixedWidth, fixedHeight) } } fun bind(vararg objects: Any) { @@ -92,6 +90,20 @@ class Window(val dev: Boolean = true) { fun run() { + var isGenerateOnly = false + if (args.size == 1) { + isGenerateOnly = args[0] == "generate" + } + + if (isGenerateOnly) { + val cg = CodeGenerator() + cg.generateTypes(*_bindObjects.toTypedArray()) + cg.generateFunctions(*_bindObjects.toTypedArray()) + cg.generateEventsAPI() + exitProcess(0) + } + + // I know, Oh no, duplicate code... if (dev) { val cg = CodeGenerator() cg.generateTypes(*_bindObjects.toTypedArray()) @@ -101,7 +113,9 @@ class Window(val dev: Boolean = true) { var server: NettyApplicationEngine? = null if (!dev) { - val prodPort = 4567 + val s = ServerSocket(0); + val prodPort = s.localPort + s.close() server = embeddedServer(Netty, port = prodPort, host = "localhost") { routing { staticResources("/", "dist") { @@ -111,14 +125,15 @@ class Window(val dev: Boolean = true) { } } server.start() - _url = "http://localhost:$prodPort" + _webviewInitFunctions.add { it.loadURL("http://localhost:$prodPort") } } + val _webview = Webview(dev) + _webviewInitFunctions.forEach { it.invoke(_webview) } MethodBinder.bind(_webview, *_bindObjects.toTypedArray()) Ipc.setWebview(_webview) _beforeStartCallbacks.forEach(Consumer { it.run() }) - _webview.loadURL(_url) _webview.run() _onCloseCallbacks.forEach(Consumer { it.run() }) diff --git a/src/test/kotlin/Main.kt b/src/test/kotlin/Main.kt index e71a081..f39fbc0 100644 --- a/src/test/kotlin/Main.kt +++ b/src/test/kotlin/Main.kt @@ -6,23 +6,21 @@ class App( val name: String = "", val age: Int = 0, val list: List = emptyList(), - val set: Set = emptySet(), val map: Map = emptyMap(), - val complex: Map>> = emptyMap() ) -fun main() { - val win = Window(dev = true) - win.setSize(700, 700) - win.setTitle("My first Javatron app!") - - win.setURL("http://localhost:5173") - win.bind( - App() - ) - - win.addBeforeStartCallback { println("Started app...") } - win.addOnCloseCallback { println("Closed the app!") } - - win.run() +fun main(args: Array) { +// val win = Window(args = args) +// win.setSize(700, 700) +// win.setTitle("My first Javatron app!") +// +// win.setURL("http://localhost:5173") +// win.bind( +// App() +// ) +// +// win.addBeforeStartCallback { println("Started app...") } +// win.addOnCloseCallback { println("Closed the app!") } +// +// win.run() }