diff --git a/.gitignore b/.gitignore
index 53ac882c..0e4abe60 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,4 +35,7 @@ project/plugins/project/
/wiki/
# Plugin Data
-data/
\ No newline at end of file
+data/
+
+# Built gui
+/src/main/resources/chatoverflow-gui
\ No newline at end of file
diff --git a/.idea/runConfigurations/Build_GUI__sbt_gui_.xml b/.idea/runConfigurations/Build_GUI__sbt_gui_.xml
new file mode 100644
index 00000000..2ec92ccf
--- /dev/null
+++ b/.idea/runConfigurations/Build_GUI__sbt_gui_.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/_Advanced__Full_Reload_and_Run_ChatOverflow.xml b/.idea/runConfigurations/_Advanced__Full_Reload_and_Run_ChatOverflow.xml
index 401c9115..c34bbadd 100644
--- a/.idea/runConfigurations/_Advanced__Full_Reload_and_Run_ChatOverflow.xml
+++ b/.idea/runConfigurations/_Advanced__Full_Reload_and_Run_ChatOverflow.xml
@@ -11,6 +11,7 @@
+
diff --git a/.idea/runConfigurations/_Deploy__Generate_Bootstrap_Launcher_and_deploy.xml b/.idea/runConfigurations/_Deploy__Generate_Bootstrap_Launcher_and_deploy.xml
index 490e4e6f..c26a0458 100644
--- a/.idea/runConfigurations/_Deploy__Generate_Bootstrap_Launcher_and_deploy.xml
+++ b/.idea/runConfigurations/_Deploy__Generate_Bootstrap_Launcher_and_deploy.xml
@@ -5,6 +5,7 @@
+
diff --git a/.idea/runConfigurations/_Simple__Rebuild_plugins_and_Run_ChatOverflow.xml b/.idea/runConfigurations/_Simple__Rebuild_plugins_and_Run_ChatOverflow.xml
index 4dd555f7..3b605953 100644
--- a/.idea/runConfigurations/_Simple__Rebuild_plugins_and_Run_ChatOverflow.xml
+++ b/.idea/runConfigurations/_Simple__Rebuild_plugins_and_Run_ChatOverflow.xml
@@ -13,4 +13,4 @@
-
\ No newline at end of file
+
diff --git a/Makefile b/Makefile
index 3fcec022..4a27a040 100644
--- a/Makefile
+++ b/Makefile
@@ -3,6 +3,7 @@
advanced_run:
sbt clean
sbt compile
+ sbt gui
sbt fetch
sbt reload
sbt version
@@ -10,11 +11,13 @@ advanced_run:
simple_run:
sbt compile
+ sbt gui
sbt package copy
bootstrap_deploy:
- sbt compile
sbt clean
+ sbt compile
+ sbt gui
sbt package copy
sbt bs "project bootstrapProject" assembly
sbt deploy
diff --git a/build.sbt b/build.sbt
index 28ea907c..bfb3852a 100644
--- a/build.sbt
+++ b/build.sbt
@@ -73,6 +73,7 @@ lazy val pluginBuildFileName = settingKey[String]("The filename of the plugin bu
lazy val pluginFolderNames = settingKey[List[String]]("The folder names of all plugin source directories.")
lazy val pluginTargetFolderNames = settingKey[List[String]]("The folder names of compiled and packaged plugins. Remember to gitignore these!")
lazy val apiProjectPath = settingKey[String]("The path to the api sub project. Remember to gitignore it!")
+lazy val guiProjectPath = settingKey[String]("The path of the Angular gui.")
// Plugin framework tasks
lazy val create = TaskKey[Unit]("create", "Creates a new plugin. Interactive command using the console.")
@@ -80,11 +81,13 @@ lazy val fetch = TaskKey[Unit]("fetch", "Searches for plugins in plugin director
lazy val copy = TaskKey[Unit]("copy", "Copies all packaged plugin jars to the target plugin folder.")
lazy val bs = TaskKey[Unit]("bs", "Updates the bootstrap project with current dependencies and chat overflow jars.")
lazy val deploy = TaskKey[Unit]("deploy", "Prepares the environment for deployment, fills deploy folder.")
+lazy val gui = TaskKey[Unit]("gui", "Installs GUI dependencies and builds it using npm.")
pluginBuildFileName := "plugins.sbt"
pluginFolderNames := List("plugins-public")
pluginTargetFolderNames := List("plugins", s"target/scala-$scalaMajorVersion/plugins")
apiProjectPath := "api"
+guiProjectPath := "gui"
create := BuildUtility(streams.value.log).createPluginTask(pluginFolderNames.value)
fetch := BuildUtility(streams.value.log).fetchPluginsTask(pluginFolderNames.value, pluginBuildFileName.value,
@@ -92,6 +95,7 @@ fetch := BuildUtility(streams.value.log).fetchPluginsTask(pluginFolderNames.valu
copy := BuildUtility(streams.value.log).copyPluginsTask(pluginFolderNames.value, pluginTargetFolderNames.value, scalaMajorVersion)
bs := BootstrapUtility.bootstrapGenTask(streams.value.log, s"$scalaMajorVersion$scalaMinorVersion", getDependencyList.value)
deploy := BootstrapUtility.prepareDeploymentTask(streams.value.log, scalaMajorVersion)
+gui := BuildUtility(streams.value.log).guiTask(guiProjectPath.value, streams.value.cacheDirectory / "gui")
// ---------------------------------------------------------------------------------------------------------------------
// UTIL
@@ -107,4 +111,8 @@ lazy val getDependencyList = Def.task[List[ModuleID]] {
} else {
updateReport.get.modules.map(m => m.module).toList
}
-}
\ No newline at end of file
+}
+
+// Clears the built GUI dirs on clean
+cleanFiles += baseDirectory.value / guiProjectPath.value / "dist"
+cleanFiles += baseDirectory.value / "src" / "main" / "resources" / "chatoverflow-gui"
\ No newline at end of file
diff --git a/project/BuildUtility.scala b/project/BuildUtility.scala
index 21f23b5e..24ffa0b8 100644
--- a/project/BuildUtility.scala
+++ b/project/BuildUtility.scala
@@ -2,6 +2,7 @@ import java.io.{File, IOException}
import java.nio.file.Files
import sbt.internal.util.ManagedLogger
+import sbt.util.{FileFunction, FilesInfo}
/**
* A build utility instance handles build tasks and prints debug information using the managed logger.
@@ -20,6 +21,7 @@ import sbt.internal.util.ManagedLogger
* | -> -> -> build.sbt
* | -> -> -> source etc.
* | -> another plugin source directory (optional)
+ * | -> gui project
*
*/
class BuildUtility(logger: ManagedLogger) {
@@ -219,6 +221,122 @@ class BuildUtility(logger: ManagedLogger) {
}
}
}
+
+ def guiTask(guiProjectPath: String, cacheDir: File): Unit = {
+ withTaskInfo("BUILD GUI") {
+ val guiDir = new File(guiProjectPath)
+ if (!guiDir.exists()) {
+ logger warn s"GUI not found at $guiProjectPath, ignoring GUI build."
+ return
+ }
+
+ if (installGuiDeps(guiDir, cacheDir).isEmpty)
+ return // Early return on failure, error has already been displayed
+
+ val outDir = buildGui(guiDir, cacheDir)
+ if (outDir.isEmpty)
+ return // Again early return on failure
+
+ // Copy built gui into resources, will be included in the classpath on execution of the framework
+ sbt.IO.copyDirectory(outDir.get, new File("src/main/resources/chatoverflow-gui"))
+ }
+ }
+
+ /**
+ * Download the dependencies of the gui using npm.
+ *
+ * @param guiDir the directory of the gui.
+ * @param cacheDir a dir, where sbt can store files for caching in the "install" sub-dir.
+ * @return None, if a error occurs which will be displayed, otherwise the output directory with the built gui.
+ */
+ private def installGuiDeps(guiDir: File, cacheDir: File): Option[File] = {
+ // Check buildGui for a explanation, it's almost the same.
+
+ val install = FileFunction.cached(new File(cacheDir, "install"), FilesInfo.hash)(_ => {
+
+ logger info "Installing GUI dependencies."
+
+ val exitCode = new ProcessBuilder(getNpmCommand :+ "install": _*)
+ .inheritIO()
+ .directory(guiDir)
+ .start()
+ .waitFor()
+
+ if (exitCode != 0) {
+ logger error "GUI dependencies couldn't be installed, please check above log for further details."
+ return None
+ } else {
+ logger info "GUI dependencies successfully installed."
+ Set(new File(guiDir, "node_modules"))
+ }
+ })
+
+ val input = new File(guiDir, "package.json")
+ install(Set(input)).headOption
+ }
+
+ /**
+ * Builds the gui using npm.
+ *
+ * @param guiDir the directory of the gui.
+ * @param cacheDir a dir, where sbt can store files for caching in the "build" sub-dir.
+ * @return None, if a error occurs which will be displayed, otherwise the output directory with the built gui.
+ */
+ private def buildGui(guiDir: File, cacheDir: File): Option[File] = {
+ // sbt allows easily to cache our external build using FileFunction.cached
+ // sbt will only invoke the passed function when at least one of the input files (passed in the last line of this method)
+ // has been modified. For the gui these input files are all files in the src directory of the gui and the package.json.
+ // sbt passes these input files to the passed function, but they aren't used, we just instruct npm to build the gui.
+ // sbt invalidates the cache as well if any of the output files (returned by the passed function) doesn't exist anymore.
+
+ val build = FileFunction.cached(new File(cacheDir, "build"), FilesInfo.hash)(_ => {
+
+ logger info "Building GUI."
+
+ val buildExitCode = new ProcessBuilder(getNpmCommand :+ "run" :+ "build": _*)
+ .inheritIO()
+ .directory(guiDir)
+ .start()
+ .waitFor()
+
+ if (buildExitCode != 0) {
+ logger error "GUI couldn't be built, please check above log for further details."
+ return None
+ } else {
+ logger info "GUI successfully built."
+ Set(new File(guiDir, "dist"))
+ }
+ })
+
+
+ val srcDir = new File(guiDir, "src")
+ val packageJson = new File(guiDir, "package.json")
+ val inputs = recursiveFileListing(srcDir) + packageJson
+
+ build(inputs).headOption
+ }
+
+ private def getNpmCommand: List[String] = {
+ if (System.getProperty("os.name").toLowerCase().contains("win")) {
+ List("cmd.exe", "/C", "npm")
+ } else {
+ List("npm")
+ }
+ }
+
+ /**
+ * Creates a file listing with all files including files in any sub-dir.
+ *
+ * @param f the directory for which the file listing needs to be created.
+ * @return the file listing as a set of files.
+ */
+ private def recursiveFileListing(f: File): Set[File] = {
+ if (f.isDirectory) {
+ f.listFiles().flatMap(recursiveFileListing).toSet
+ } else {
+ Set(f)
+ }
+ }
}
object BuildUtility {
diff --git a/src/main/scala/org/codeoverflow/chatoverflow/ui/web/Server.scala b/src/main/scala/org/codeoverflow/chatoverflow/ui/web/Server.scala
index 5a89cab0..d8d65a1e 100644
--- a/src/main/scala/org/codeoverflow/chatoverflow/ui/web/Server.scala
+++ b/src/main/scala/org/codeoverflow/chatoverflow/ui/web/Server.scala
@@ -1,7 +1,7 @@
package org.codeoverflow.chatoverflow.ui.web
import org.codeoverflow.chatoverflow.{ChatOverflow, WithLogger}
-import org.eclipse.jetty.servlet.ServletHandler.Default404Servlet
+import org.eclipse.jetty.util.resource.Resource
import org.eclipse.jetty.webapp.WebAppContext
import org.scalatra.servlet.ScalatraListener
@@ -17,9 +17,8 @@ class Server(val chatOverflow: ChatOverflow, val port: Int) extends WithLogger {
private val context = new WebAppContext()
context.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false")
context setContextPath "/"
- context.setResourceBase("/")
+ context.setBaseResource(Resource.newClassPathResource("/chatoverflow-gui/"))
context.addEventListener(new ScalatraListener)
- context.addServlet(classOf[Default404Servlet], "/")
server.setHandler(context)