diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1bd3353
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,132 @@
+# Created by .ignore support plugin (hsz.mobi)
+### Eclipse template
+*.pydevproject
+.metadata
+.gradle
+bin/
+tmp/
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.settings/
+.loadpath
+
+# Eclipse Core
+.project
+
+# External tool builders
+.externalToolBuilders/
+
+# Locally stored "Eclipse launch configurations"
+*.launch
+
+# CDT-specific
+.cproject
+
+# JDT-specific (Eclipse Java Development Tools)
+.classpath
+
+# Java annotation processor (APT)
+.factorypath
+
+# PDT-specific
+.buildpath
+
+# sbteclipse plugin
+.target
+
+# TeXlipse plugin
+.texlipse
+### Maven template
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
+
+*.iml
+
+## Directory-based project format:
+.idea/
+# if you remove the above rule, at least ignore the following:
+
+# User-specific stuff:
+# .idea/workspace.xml
+# .idea/tasks.xml
+# .idea/dictionaries
+
+# Sensitive or high-churn files:
+# .idea/dataSources.ids
+# .idea/dataSources.xml
+# .idea/sqlDataSources.xml
+# .idea/dynamic.xml
+# .idea/uiDesigner.xml
+
+# Gradle:
+# .idea/gradle.xml
+# .idea/libraries
+
+# Mongo Explorer plugin:
+# .idea/mongoSettings.xml
+
+## File-based project format:
+*.ipr
+*.iws
+
+## Plugin-specific files:
+
+# IntelliJ
+/out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+### Java template
+*.class
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.ear
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+### Scala template
+*.class
+*.log
+
+# sbt specific
+.cache
+.history
+.lib/
+dist/*
+!dist/run.bat
+!dist/run.sh
+target/
+lib_managed/
+src_managed/
+project/boot/
+project/plugins/project/
+
+# Scala-IDE specific
+.scala_dependencies
+.worksheet
+
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..6de00a3
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,7 @@
+language: scala
+
+scala:
+ - 2.11.7
+
+script:
+ - ./test.sh
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..1770b14
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# Udash Generator [![Build Status](https://travis-ci.org/UdashFramework/udash-generator.svg?branch=master)](https://travis-ci.org/UdashFramework/udash-generator) [![Join the chat at https://gitter.im/UdashFramework/udash-generator](https://badges.gitter.im/UdashFramework/udash-generator.svg)](https://gitter.im/UdashFramework/udash-generator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](http://www.avsystem.com/)
+
+Project generator for the Udash framework.
\ No newline at end of file
diff --git a/build.sbt b/build.sbt
new file mode 100644
index 0000000..4cab99d
--- /dev/null
+++ b/build.sbt
@@ -0,0 +1,19 @@
+name := "udash-generator"
+
+version in ThisBuild := "0.1.0"
+organization in ThisBuild := "io.udash"
+scalaVersion in ThisBuild := "2.11.7"
+
+lazy val generator = project.in(file("."))
+ .aggregate(core, cmd)
+ .settings(publishArtifact := false)
+
+lazy val core = project.in(file("core"))
+ .settings(libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.0-M15" % Test)
+
+lazy val cmd = project.in(file("cmd"))
+ .dependsOn(core)
+ .settings(
+ assemblyJarName in assembly := "udash-generator.jar",
+ mainClass in assembly := Some("io.udash.generator.Launcher")
+ )
\ No newline at end of file
diff --git a/cmd/src/main/scala/io/udash/generator/CmdDecisionMaker.scala b/cmd/src/main/scala/io/udash/generator/CmdDecisionMaker.scala
new file mode 100644
index 0000000..7aeb1c0
--- /dev/null
+++ b/cmd/src/main/scala/io/udash/generator/CmdDecisionMaker.scala
@@ -0,0 +1,85 @@
+package io.udash.generator
+
+import java.io.File
+
+import io.udash.generator.configuration._
+import io.udash.generator.exceptions.InvalidConfigDecisionResponse
+
+import scala.io.StdIn
+
+class CmdDecisionMaker extends DecisionMaker {
+ private val yesAnswers = Seq("yes", "y", "true", "t")
+
+ override def makeDecision[T](d: Decision[T]): Decision[T] = {
+ try {
+ val response: Decision[T] = d match {
+ case decision@RootDirectory(_) =>
+ RootDirectory(Some(askForFile("Project root directory", decision.default)))
+ case decision@ClearRootDirectory(_) =>
+ ClearRootDirectory(Some(askForBoolean("Clear root directory", decision.default)))
+ case decision@ProjectName(_) =>
+ ProjectName(Some(askForString("Project name", decision.default)))
+ case decision@Organization(_) =>
+ Organization(Some(askForString("Organization", decision.default)))
+ case decision@RootPackage(_) =>
+ RootPackage(Some(askForPackage("Root package", decision.default)))
+ case decision@ProjectTypeSelect(_) =>
+ ProjectTypeSelect(Some(askForSelection("Project type", decision.default, decision.options)))
+ case decision@StdProjectTypeModulesSelect(_) =>
+ val backend = askForString("Backend module name", decision.default.backend)
+ val shared = askForString("Shared module name", decision.default.shared)
+ val frontend = askForString("Frontend module name", decision.default.frontend)
+ StdProjectTypeModulesSelect(Some(StandardProject(backend, shared, frontend)))
+ case decision@CreateBasicFrontendApp(_) =>
+ CreateBasicFrontendApp(Some(askForBoolean("Create basic frontend application", decision.default)))
+ case decision@CreateFrontendDemos(_) =>
+ CreateFrontendDemos(Some(askForBoolean("Create frontend demo views", decision.default)))
+ case decision@CreateScalaCSSDemos(_) =>
+ CreateScalaCSSDemos(Some(askForBoolean("Create ScalaCSS demo views", decision.default)))
+ case decision@CreateJettyLauncher(_) =>
+ CreateJettyLauncher(Some(askForBoolean("Create Jetty launcher", decision.default)))
+ case decision@CreateRPC(_) =>
+ CreateRPC(Some(askForBoolean("Create RPC communication layer", decision.default)))
+ case decision@CreateRPCDemos(_) =>
+ CreateRPCDemos(Some(askForBoolean("Create RPC communication layer demos", decision.default)))
+ case decision@RunGenerator(_) =>
+ RunGenerator(Some(askForBoolean("Start generation", decision.default)))
+ }
+ for (errors <- response.validator()) throw InvalidConfigDecisionResponse(errors)
+ response
+ } catch {
+ case InvalidConfigDecisionResponse(ex) =>
+ println(ex)
+ makeDecision(d)
+ case _: Exception =>
+ makeDecision(d)
+ }
+ }
+
+ private def ask[T](prompt: String, default: T)(converter: String => T): T = {
+ val response = StdIn.readLine(prompt).trim
+ if (response.isEmpty) default else converter(response)
+ }
+
+ private def askWithDefault[T](prompt: String, default: T)(converter: String => T): T =
+ ask(s"$prompt [$default]: ", default)(converter)
+
+ private def askForString(prompt: String, default: String): String =
+ askWithDefault(prompt, default)(s => s)
+
+ private def askForBoolean(prompt: String, default: Boolean): Boolean =
+ askWithDefault(prompt, default)(r => yesAnswers.contains(r.toLowerCase))
+
+ private def askForPackage(prompt: String, default: Seq[String]): Seq[String] =
+ ask(s"$prompt [${default.mkString(".")}]: ", default)(s => s.split("\\."))
+
+ private def askForFile(prompt: String, default: File): File =
+ ask(s"$prompt [${default.getAbsolutePath}]: ", default)(r => new File(r))
+
+ private def askForSelection[T](prompt: String, default: T, options: Seq[T]): T = {
+ val optionsPresentation = options.zipWithIndex.map{
+ case decision@(opt, idx) => s" ${idx+1}. $opt \n"
+ }.mkString
+ ask(s"$prompt [${options.indexOf(default)}]:\n${optionsPresentation}Select: ", default)(r => options(Integer.parseInt(r) - 1))
+ }
+}
diff --git a/cmd/src/main/scala/io/udash/generator/Launcher.scala b/cmd/src/main/scala/io/udash/generator/Launcher.scala
new file mode 100644
index 0000000..703dd84
--- /dev/null
+++ b/cmd/src/main/scala/io/udash/generator/Launcher.scala
@@ -0,0 +1,13 @@
+package io.udash.generator
+
+import io.udash.generator.configuration.ConfigurationBuilder
+
+object Launcher {
+ def main(args: Array[String]) {
+ val generator = new Generator
+ val configBuilder = new ConfigurationBuilder(new CmdDecisionMaker)
+ val config = configBuilder.build()
+
+ generator.start(config.plugins, config.settings)
+ }
+}
diff --git a/core/src/main/scala/io/udash/generator/Generator.scala b/core/src/main/scala/io/udash/generator/Generator.scala
new file mode 100644
index 0000000..d7c9a4d
--- /dev/null
+++ b/core/src/main/scala/io/udash/generator/Generator.scala
@@ -0,0 +1,17 @@
+package io.udash.generator
+
+import io.udash.generator.utils.FileOps
+
+class Generator extends FileOps {
+ /**
+ * Starts project generation process.
+ *
+ * @param plugins Sequence of generator plugins, which will be fired.
+ * @param settings Initial project settings.
+ */
+ def start(plugins: Seq[GeneratorPlugin], settings: GeneratorSettings): GeneratorSettings = {
+ if (settings.shouldRemoveExistingData) removeFileOrDir(settings.rootDirectory)
+ settings.rootDirectory.mkdirs()
+ plugins.foldLeft(settings)((settings: GeneratorSettings, plugin: GeneratorPlugin) => plugin.run(settings))
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/scala/io/udash/generator/GeneratorPlugin.scala b/core/src/main/scala/io/udash/generator/GeneratorPlugin.scala
new file mode 100644
index 0000000..944688c
--- /dev/null
+++ b/core/src/main/scala/io/udash/generator/GeneratorPlugin.scala
@@ -0,0 +1,11 @@
+package io.udash.generator
+
+import io.udash.generator.utils.FileOps
+
+/** Part of generation chain. */
+trait GeneratorPlugin extends FileOps {
+ /** Starts plugins work with current project settings.
+ *
+ * @return Project settings which will be passed to next plugin in generator sequence. */
+ def run(settings: GeneratorSettings): GeneratorSettings
+}
diff --git a/core/src/main/scala/io/udash/generator/GeneratorSettings.scala b/core/src/main/scala/io/udash/generator/GeneratorSettings.scala
new file mode 100644
index 0000000..dc66b1b
--- /dev/null
+++ b/core/src/main/scala/io/udash/generator/GeneratorSettings.scala
@@ -0,0 +1,66 @@
+package io.udash.generator
+
+import java.io.File
+
+/**
+ * Basic project configuration.
+ *
+ * @param rootDirectory Root directory of whole project.
+ * @param shouldRemoveExistingData If `true`, generator will remove whole data from rootDirectory.
+ * @param rootPackage Root package of sources.
+ * @param projectType Project modules configuration.
+ * @param organization Organization name.
+ * @param projectName Project name.
+ */
+case class GeneratorSettings(rootDirectory: File,
+ shouldRemoveExistingData: Boolean,
+ projectName: String,
+ organization: String,
+ projectType: ProjectType,
+ rootPackage: Seq[String]) {
+ /** Root package of views in frontend. */
+ def viewsSubPackage: Seq[String] = Seq("views")
+ /** Root package of styles in frontend. */
+ def stylesSubPackage: Seq[String] = Seq("styles")
+
+ def scalaVersion: String = "2.11.7"
+ def sbtVersion: String = "0.13.9"
+ def scalaJSVersion: String = "0.6.7"
+ def scalaCSSVersion: String = "0.4.0"
+ def udashCoreVersion: String = "0.1.0"
+ def udashRPCVersion: String = "0.1.0"
+ def jettyVersion: String = "9.3.7.v20160115"
+ def logbackVersion: String = "1.1.3"
+
+ /** Application HTML root element id */
+ def htmlRootId: String = "application"
+
+ /** Generated JS file with application code name (dev). */
+ def frontendImplFastJs: String = "frontend-impl-fast.js"
+ /** Generated JS file with application code name (prod). */
+ def frontendImplJs: String = "frontend-impl.js"
+ /** Generated JS file with dependencies code name (dev). */
+ def frontendDepsFastJs: String = "frontend-deps-fast.js"
+ /** Generated JS file with dependencies code name (prod). */
+ def frontendDepsJs: String = "frontend-deps.js"
+ /** Generated JS file with app launcher name (dev). */
+ def frontendInitJs: String = "frontend-init.js"
+
+ /** Udash DevGuide root URL. */
+ //TODO: add developer guide url
+ //def udashDevGuide: String = ""
+}
+
+sealed trait ProjectType
+
+/** Project does not contain submodules, it's only frontend application and everything is compiled to JavaScript. */
+case object FrontendOnlyProject extends ProjectType
+
+/**
+ * Standard Udash project with three submodules:
+ *
+ * @param backend - module compiled to JVM name
+ * @param shared - module compiled to JS and JVM name
+ * @param frontend - module compiled to JS name
+ */
+case class StandardProject(backend: String, shared: String, frontend: String) extends ProjectType
diff --git a/core/src/main/scala/io/udash/generator/configuration/ConfigurationBuilder.scala b/core/src/main/scala/io/udash/generator/configuration/ConfigurationBuilder.scala
new file mode 100644
index 0000000..a285c45
--- /dev/null
+++ b/core/src/main/scala/io/udash/generator/configuration/ConfigurationBuilder.scala
@@ -0,0 +1,121 @@
+package io.udash.generator.configuration
+
+import io.udash.generator._
+import io.udash.generator.exceptions.InvalidConfigDecisionResponse
+import io.udash.generator.plugins.PlaceholdersCleanPlugin
+import io.udash.generator.plugins.core.{CoreDemosPlugin, CorePlugin}
+import io.udash.generator.plugins.jetty.JettyLauncherPlugin
+import io.udash.generator.plugins.rpc.{RPCDemosPlugin, RPCPlugin}
+import io.udash.generator.plugins.sbt.{SBTBootstrapPlugin, SBTModulesPlugin}
+import io.udash.generator.plugins.scalacss.ScalaCSSDemosPlugin
+
+import scala.annotation.tailrec
+
+trait DecisionMaker {
+ /** Should return decision with filled response option. */
+ def makeDecision[T](decision: Decision[T]): Decision[T]
+}
+
+case class Configuration(plugins: Seq[GeneratorPlugin], settings: GeneratorSettings)
+
+/** Creates project configuration based on `decisionMaker` responses. */
+class ConfigurationBuilder(decisionMaker: DecisionMaker) {
+ def build(): Configuration = {
+ @tailrec
+ def _build(plugins: Seq[GeneratorPlugin], settings: GeneratorSettings)(decisions: List[Decision[_]]): Configuration = {
+ if (decisions.isEmpty) Configuration(plugins, settings)
+ else {
+ val response = decisionMaker.makeDecision(decisions.head)
+ val errors: Option[String] = response.validator()
+ if (errors.isDefined) throw InvalidConfigDecisionResponse(errors.get)
+ _build(
+ plugins ++ selectPlugins(response),
+ changeSettings(response, settings)
+ )(selectNextDecisions(response, settings) ++ decisions.tail)
+ }
+ }
+
+ _build(Seq(), GeneratorSettings(null, false, null, null, null, null))(startingDecisions)
+ }
+
+ private def selectPlugins(response: Decision[_]): Seq[GeneratorPlugin] = {
+ response match {
+ case ProjectTypeSelect(Some(FrontendOnlyProject)) =>
+ Seq(SBTBootstrapPlugin, SBTModulesPlugin)
+ case StdProjectTypeModulesSelect(Some(projectType)) =>
+ Seq(SBTBootstrapPlugin, SBTModulesPlugin)
+ case CreateBasicFrontendApp(Some(true)) =>
+ Seq(CorePlugin)
+ case CreateFrontendDemos(Some(true)) =>
+ Seq(CoreDemosPlugin)
+ case CreateScalaCSSDemos(Some(true)) =>
+ Seq(ScalaCSSDemosPlugin)
+ case CreateJettyLauncher(Some(true)) =>
+ Seq(JettyLauncherPlugin)
+ case CreateRPC(Some(true)) =>
+ Seq(RPCPlugin)
+ case CreateRPCDemos(Some(true)) =>
+ Seq(RPCDemosPlugin)
+ case RunGenerator(Some(true)) =>
+ Seq(PlaceholdersCleanPlugin)
+ case _ =>
+ Seq.empty
+ }
+ }
+
+ private def changeSettings(response: Decision[_], settings: GeneratorSettings): GeneratorSettings = {
+ response match {
+ case RootDirectory(Some(dir)) =>
+ settings.copy(rootDirectory = dir)
+ case ClearRootDirectory(Some(clear)) =>
+ settings.copy(shouldRemoveExistingData = clear)
+ case ProjectName(Some(name)) =>
+ settings.copy(projectName = name)
+ case Organization(Some(name)) =>
+ settings.copy(organization = name)
+ case RootPackage(Some(pck)) =>
+ settings.copy(rootPackage = pck)
+ case ProjectTypeSelect(Some(FrontendOnlyProject)) =>
+ settings.copy(projectType = FrontendOnlyProject)
+ case StdProjectTypeModulesSelect(Some(projectType)) =>
+ settings.copy(projectType = projectType)
+ case _ =>
+ settings
+ }
+ }
+
+ private def selectNextDecisions(response: Decision[_], settings: GeneratorSettings): List[Decision[_]] = {
+ response match {
+ case ProjectTypeSelect(Some(FrontendOnlyProject)) =>
+ List(CreateBasicFrontendApp())
+ case ProjectTypeSelect(Some(_: StandardProject)) =>
+ List(StdProjectTypeModulesSelect())
+ case StdProjectTypeModulesSelect(Some(projectType)) =>
+ List(CreateBasicFrontendApp())
+ case CreateBasicFrontendApp(Some(true)) =>
+ settings.projectType match {
+ case FrontendOnlyProject =>
+ List(CreateFrontendDemos(), CreateScalaCSSDemos())
+ case StandardProject(_, _, _) =>
+ List(CreateFrontendDemos(), CreateScalaCSSDemos(), CreateJettyLauncher())
+ }
+ case CreateJettyLauncher(Some(true)) =>
+ List(CreateRPC())
+ case CreateRPC(Some(true)) =>
+ List(CreateRPCDemos())
+ case _ =>
+ List.empty
+ }
+ }
+
+ private val startingDecisions: List[Decision[_]] =
+ List[Decision[_]](
+ RootDirectory(),
+ ClearRootDirectory(),
+ ProjectName(),
+ Organization(),
+ RootPackage(),
+ ProjectTypeSelect(),
+ RunGenerator()
+ )
+}
diff --git a/core/src/main/scala/io/udash/generator/configuration/decisions.scala b/core/src/main/scala/io/udash/generator/configuration/decisions.scala
new file mode 100644
index 0000000..e27f75f
--- /dev/null
+++ b/core/src/main/scala/io/udash/generator/configuration/decisions.scala
@@ -0,0 +1,84 @@
+package io.udash.generator.configuration
+
+import java.io.File
+
+import io.udash.generator.{FrontendOnlyProject, ProjectType, StandardProject}
+
+sealed abstract class Decision[ResponseType](val default: ResponseType) {
+ def response: Option[ResponseType]
+ def validator(): Option[String] = None
+}
+
+sealed abstract class SelectDecision[ResponseType](default: ResponseType) extends Decision[ResponseType](default) {
+ def options: Seq[ResponseType]
+ override def validator(): Option[String] =
+ if (!options.contains(response.get)) Some(s"Select one of following values: $options") else None
+}
+
+sealed abstract class NoneEmptyStringDecision(errorMsg: String, default: String) extends Decision[String](default) {
+ override def validator(): Option[String] =
+ if (response.get.isEmpty) Some(errorMsg) else None
+}
+
+/** Project will be generated in this directory. */
+case class RootDirectory(override val response: Option[File] = None) extends Decision[File](new File("udash-app")) {
+ override def validator(): Option[String] = {
+ val target = response.get
+ if (target.exists() && !target.isDirectory) Some(s"${target.getAbsolutePath} is not a directory.")
+ else if (target.exists() && !target.canWrite) Some(s"You can not write in ${target.getAbsolutePath}.")
+ else None
+ }
+}
+
+/** If `true`, root project directory will be cleared before generation. */
+case class ClearRootDirectory(override val response: Option[Boolean] = None) extends Decision[Boolean](false)
+
+/** Generated project name. */
+case class ProjectName(override val response: Option[String] = None) extends NoneEmptyStringDecision("Project name can not be empty!", "udash-app")
+
+/** Organization name. */
+case class Organization(override val response: Option[String] = None) extends NoneEmptyStringDecision("Organization name can not be empty!", "com.example")
+
+/** Root source code package. */
+case class RootPackage(override val response: Option[Seq[String]] = None) extends Decision[Seq[String]](Seq("com", "example")) {
+ override def validator(): Option[String] =
+ if (response.get.isEmpty) Some("Root package can not be empty!") else None
+}
+
+/** Frontend-only or standard Udash project configuration. */
+case class ProjectTypeSelect(override val response: Option[ProjectType] = None) extends SelectDecision[ProjectType](StandardProject("backend", "shared", "frontend")) {
+ override val options: Seq[ProjectType] = Seq(FrontendOnlyProject, StandardProject("backend", "shared", "frontend"))
+}
+
+/** Names of modules in standard Udash project configuration. */
+case class StdProjectTypeModulesSelect(override val response: Option[StandardProject] = None) extends Decision[StandardProject](StandardProject("backend", "shared", "frontend")) {
+ override def validator(): Option[String] = {
+ val project = response.get
+ if (project.backend == project.frontend || project.backend == project.shared || project.frontend == project.shared)
+ Some("Module names must be unique.")
+ else if (project.backend.isEmpty || project.shared.isEmpty || project.frontend.isEmpty)
+ Some("Module names must not be empty.")
+ else None
+ }
+}
+
+/* If `true`, generator creates basic frontend application code. */
+case class CreateBasicFrontendApp(override val response: Option[Boolean] = None) extends Decision[Boolean](true)
+
+/* If `true`, generator creates frontend demo views. */
+case class CreateFrontendDemos(override val response: Option[Boolean] = None) extends Decision[Boolean](true)
+
+/* If `true`, generator creates ScalaCSS demo views. */
+case class CreateScalaCSSDemos(override val response: Option[Boolean] = None) extends Decision[Boolean](true)
+
+/* If `true`, generator creates Jetty server serving frontend application. */
+case class CreateJettyLauncher(override val response: Option[Boolean] = None) extends Decision[Boolean](true)
+
+/* If `true`, generator creates example RPC interfaces and implementation in `frontend` and `backend` modules. */
+case class CreateRPC(override val response: Option[Boolean] = None) extends Decision[Boolean](true)
+
+/* If `true`, generator creates RPC demo views. */
+case class CreateRPCDemos(override val response: Option[Boolean] = None) extends Decision[Boolean](true)
+
+/* If `true`, generator starts project generation process. */
+case class RunGenerator(override val response: Option[Boolean] = None) extends Decision[Boolean](true)
\ No newline at end of file
diff --git a/core/src/main/scala/io/udash/generator/exceptions/package.scala b/core/src/main/scala/io/udash/generator/exceptions/package.scala
new file mode 100644
index 0000000..76827a1
--- /dev/null
+++ b/core/src/main/scala/io/udash/generator/exceptions/package.scala
@@ -0,0 +1,10 @@
+package io.udash.generator
+
+package object exceptions {
+ case class FileCreationError(msg: String) extends RuntimeException(msg)
+ case class FileDoesNotExist(msg: String) extends RuntimeException(msg)
+
+ case class InvalidConfiguration(msg: String) extends RuntimeException(msg)
+
+ case class InvalidConfigDecisionResponse(msg: String) extends RuntimeException(msg)
+}
\ No newline at end of file
diff --git a/core/src/main/scala/io/udash/generator/plugins/Placeholder.scala b/core/src/main/scala/io/udash/generator/plugins/Placeholder.scala
new file mode 100644
index 0000000..9c145bd
--- /dev/null
+++ b/core/src/main/scala/io/udash/generator/plugins/Placeholder.scala
@@ -0,0 +1,36 @@
+package io.udash.generator.plugins
+
+sealed class Placeholder(str: String) {
+ override def toString: String = s"/*<<$str>>*/"
+}
+
+case object UdashBuildPlaceholder extends Placeholder("udash-generator-custom-build")
+
+case object DependenciesPlaceholder extends Placeholder("udash-generator-dependencies")
+case object DependenciesVariablesPlaceholder extends Placeholder("udash-generator-dependencies-variables")
+case object DependenciesFrontendPlaceholder extends Placeholder("udash-generator-dependencies-frontend")
+case object DependenciesFrontendJSPlaceholder extends Placeholder("udash-generator-dependencies-frontendJS")
+case object DependenciesCrossPlaceholder extends Placeholder("udash-generator-dependencies-cross")
+case object DependenciesBackendPlaceholder extends Placeholder("udash-generator-dependencies-backend")
+
+case object RootSettingsPlaceholder extends Placeholder("udash-generator-root-settings")
+case object RootModulePlaceholder extends Placeholder("udash-generator-root-module")
+case object FrontendSettingsPlaceholder extends Placeholder("udash-generator-frontend-settings")
+case object FrontendModulePlaceholder extends Placeholder("udash-generator-frontend-module")
+case object BackendSettingsPlaceholder extends Placeholder("udash-generator-backend-settings")
+case object BackendModulePlaceholder extends Placeholder("udash-generator-backend-module")
+case object SharedSettingsPlaceholder extends Placeholder("udash-generator-shared-settings")
+case object SharedModulePlaceholder extends Placeholder("udash-generator-shared-module")
+case object SharedJSModulePlaceholder extends Placeholder("udash-generator-sharedJS-module")
+case object SharedJVMModulePlaceholder extends Placeholder("udash-generator-sharedJVM-module")
+
+case object HTMLHeadPlaceholder extends Placeholder("udash-generator-html-head")
+
+case object FrontendRoutingRegistryPlaceholder extends Placeholder("udash-generator-frontend-routing-registry")
+case object FrontendVPRegistryPlaceholder extends Placeholder("udash-generator-frontend-vp-registry")
+case object FrontendStatesRegistryPlaceholder extends Placeholder("udash-generator-frontend-states-registry")
+case object FrontendIndexMenuPlaceholder extends Placeholder("udash-generator-frontend-index-menu")
+case object FrontendContextPlaceholder extends Placeholder("udash-generator-frontend-context")
+case object FrontendAppInitPlaceholder extends Placeholder("udash-generator-frontend-app-init")
+
+case object BackendAppServerPlaceholder extends Placeholder("udash-generator-backend-app-server")
diff --git a/core/src/main/scala/io/udash/generator/plugins/PlaceholdersCleanPlugin.scala b/core/src/main/scala/io/udash/generator/plugins/PlaceholdersCleanPlugin.scala
new file mode 100644
index 0000000..e4e4b91
--- /dev/null
+++ b/core/src/main/scala/io/udash/generator/plugins/PlaceholdersCleanPlugin.scala
@@ -0,0 +1,29 @@
+package io.udash.generator.plugins
+
+import java.io.File
+
+import io.udash.generator.{GeneratorPlugin, GeneratorSettings}
+
+/**
+ * Removes all placeholders from generated project.
+ * Placeholders look like: /*<>*/
+ */
+object PlaceholdersCleanPlugin extends GeneratorPlugin {
+ override def run(settings: GeneratorSettings): GeneratorSettings = {
+ cleanDirectory(settings.rootDirectory)
+ settings
+ }
+
+ private def cleanDirectory(dir: File): Unit =
+ dir.listFiles()
+ .foreach(f =>
+ if (f.isDirectory) cleanDirectory(f)
+ else cleanFile(f)
+ )
+
+ private def cleanFile(file: File): Unit = {
+ removeFromFile(file)("/\\*<>\\*/")
+ // Clear leading comma in dependencies
+ replaceInFile(file)("\\(\\s*,", "(")
+ }
+}
diff --git a/core/src/main/scala/io/udash/generator/plugins/core/CoreDemosPlugin.scala b/core/src/main/scala/io/udash/generator/plugins/core/CoreDemosPlugin.scala
new file mode 100644
index 0000000..df10d47
--- /dev/null
+++ b/core/src/main/scala/io/udash/generator/plugins/core/CoreDemosPlugin.scala
@@ -0,0 +1,97 @@
+package io.udash.generator.plugins.core
+
+import java.io.File
+
+import io.udash.generator.plugins._
+import io.udash.generator.plugins.sbt.SBTProjectFiles
+import io.udash.generator.plugins.utils.{FrontendPaths, UtilPaths}
+import io.udash.generator.utils._
+import io.udash.generator.{FrontendOnlyProject, GeneratorPlugin, GeneratorSettings, StandardProject}
+
+object CoreDemosPlugin extends GeneratorPlugin with SBTProjectFiles with FrontendPaths with UtilPaths {
+
+ override def run(settings: GeneratorSettings): GeneratorSettings = {
+ val rootPck: File = settings.projectType match {
+ case FrontendOnlyProject =>
+ rootPackageInSrc(settings.rootDirectory, settings)
+ case StandardProject(_, shared, frontend) =>
+ rootPackageInSrc(settings.rootDirectory.subFile(frontend), settings)
+ }
+ val stateName = createDemoView(rootPck, settings)
+ addIndexLink(rootPck, stateName)
+
+ settings
+ }
+
+ private def addIndexLink(rootPackage: File, state: String): Unit = {
+ val indexViewScala = viewsPackageInSrc(rootPackage).subFile("IndexView.scala")
+ requireFilesExist(Seq(indexViewScala))
+
+ appendOnPlaceholder(indexViewScala)(FrontendIndexMenuPlaceholder,
+ s""",
+ | li(a(href := $state().url)("Binding demo")),
+ | li(a(href := $state("From index").url)("Binding demo with URL argument"))""".stripMargin)
+ }
+
+ private def createDemoView(rootPackage: File, settings: GeneratorSettings): String = {
+ val statesScala = rootPackage.subFile("states.scala")
+ val routingRegistryDefScala = rootPackage.subFile("RoutingRegistryDef.scala")
+ val statesToViewPresenterDefScala = rootPackage.subFile("StatesToViewPresenterDef.scala")
+
+ val bindingDemoViewScala = viewsPackageInSrc(rootPackage).subFile("BindingDemoView.scala")
+ val stateName = "BindingDemoState"
+
+ requireFilesExist(Seq(viewsPackageInSrc(rootPackage), statesScala, routingRegistryDefScala, statesToViewPresenterDefScala))
+ createFiles(Seq(bindingDemoViewScala), requireNotExists = true)
+
+ appendOnPlaceholder(statesScala)(FrontendStatesRegistryPlaceholder,
+ s"""
+ |
+ |case class $stateName(urlArg: String = "") extends RoutingState(RootState)""".stripMargin)
+
+ appendOnPlaceholder(routingRegistryDefScala)(FrontendRoutingRegistryPlaceholder,
+ s"""
+ | case "/binding" => $stateName("")
+ | case "/binding" /:/ arg => $stateName(arg)""".stripMargin)
+
+ appendOnPlaceholder(statesToViewPresenterDefScala)(FrontendVPRegistryPlaceholder,
+ s"""
+ | case $stateName(urlArg) => BindingDemoViewPresenter(urlArg)""".stripMargin)
+
+ writeFile(bindingDemoViewScala)(
+ s"""package ${settings.rootPackage.mkPackage()}.${settings.viewsSubPackage.mkPackage()}
+ |
+ |import io.udash._
+ |import ${settings.rootPackage.mkPackage()}.$stateName
+ |import org.scalajs.dom.Element
+ |
+ |case class BindingDemoViewPresenter(urlArg: String) extends DefaultViewPresenterFactory[$stateName](() => {
+ | import ${settings.rootPackage.mkPackage()}.Context._
+ |
+ | val model = Property[String](urlArg)
+ | new BindingDemoView(model)
+ |})
+ |
+ |class BindingDemoView(model: Property[String]) extends View {
+ | import scalatags.JsDom.all._
+ |
+ | private val content = div(
+ | div(
+ | "You can find this demo source code in: ",
+ | i("${settings.rootPackage.mkPackage()}.${settings.viewsSubPackage.mkPackage()}.BindingDemoView")
+ | ),
+ | h3("Example"),
+ | TextInput(model, placeholder := "Type something..."),
+ | div("You typed: ", bind(model))
+ | ).render
+ |
+ | override def getTemplate: Element = content
+ |
+ | override def renderChild(view: View): Unit = {}
+ |}
+ |""".stripMargin
+ )
+
+ stateName
+ }
+}
diff --git a/core/src/main/scala/io/udash/generator/plugins/core/CorePlugin.scala b/core/src/main/scala/io/udash/generator/plugins/core/CorePlugin.scala
new file mode 100644
index 0000000..a113466
--- /dev/null
+++ b/core/src/main/scala/io/udash/generator/plugins/core/CorePlugin.scala
@@ -0,0 +1,229 @@
+package io.udash.generator.plugins.core
+
+import java.io.File
+
+import io.udash.generator.plugins._
+import io.udash.generator.plugins.sbt.SBTProjectFiles
+import io.udash.generator.plugins.utils.{FrontendPaths, UtilPaths}
+import io.udash.generator.utils._
+import io.udash.generator.{FrontendOnlyProject, GeneratorPlugin, GeneratorSettings, StandardProject}
+
+object CorePlugin extends GeneratorPlugin with SBTProjectFiles with FrontendPaths with UtilPaths {
+ override def run(settings: GeneratorSettings): GeneratorSettings = {
+ settings.projectType match {
+ case FrontendOnlyProject =>
+ addUdashCoreDependency(settings)
+ createJSApp(rootPackageInSrc(settings.rootDirectory, settings), settings)
+ case StandardProject(_, shared, frontend) =>
+ addUdashCoreDependency(settings)
+ createJSApp(rootPackageInSrc(settings.rootDirectory.subFile(frontend), settings), settings)
+ }
+
+ settings
+ }
+
+ private def addUdashCoreDependency(settings: GeneratorSettings): Unit = {
+ requireFilesExist(Seq(dependenciesScala(settings)))
+
+ appendOnPlaceholder(dependenciesScala(settings))(DependenciesVariablesPlaceholder,
+ s"""val udashCoreVersion = "${settings.udashCoreVersion}"
+ |val logbackVersion = "${settings.logbackVersion}"""".stripMargin)
+
+ appendOnPlaceholder(dependenciesScala(settings))(DependenciesCrossPlaceholder,
+ s""",
+ | "io.udash" % "udash-core-shared" % udashCoreVersion""".stripMargin)
+
+ appendOnPlaceholder(dependenciesScala(settings))(DependenciesFrontendPlaceholder,
+ s""",
+ | "io.udash" %%% "udash-core-frontend" % udashCoreVersion""".stripMargin)
+
+ appendOnPlaceholder(dependenciesScala(settings))(DependenciesBackendPlaceholder,
+ s""",
+ | "ch.qos.logback" % "logback-classic" % logbackVersion""".stripMargin)
+ }
+
+ private def createJSApp(rootPackage: File, settings: GeneratorSettings): Unit = {
+ val initScala = rootPackage.subFile("init.scala")
+ val statesScala = rootPackage.subFile("states.scala")
+ val routingRegistryDefScala = rootPackage.subFile("RoutingRegistryDef.scala")
+ val statesToViewPresenterDefScala = rootPackage.subFile("StatesToViewPresenterDef.scala")
+ val rootViewScala = viewsPackageInSrc(rootPackage).subFile("RootView.scala")
+ val indexViewScala = viewsPackageInSrc(rootPackage).subFile("IndexView.scala")
+ val errorViewScala = viewsPackageInSrc(rootPackage).subFile("ErrorView.scala")
+
+ createDirs(Seq(viewsPackageInSrc(rootPackage)))
+ createFiles(Seq(initScala, statesScala, routingRegistryDefScala, statesToViewPresenterDefScala,
+ rootViewScala, indexViewScala, errorViewScala), requireNotExists = true)
+
+ writeFile(initScala)(
+ s"""package ${settings.rootPackage.mkPackage()}
+ |
+ |import io.udash._
+ |import io.udash.wrappers.jquery._
+ |import org.scalajs.dom.{Element, document}
+ |
+ |import scala.scalajs.js.JSApp
+ |import scala.scalajs.js.annotation.JSExport
+ |
+ |object Context {
+ | implicit val executionContext = scalajs.concurrent.JSExecutionContext.Implicits.queue
+ | private val routingRegistry = new RoutingRegistryDef
+ | private val viewPresenterRegistry = new StatesToViewPresenterDef
+ |
+ | implicit val applicationInstance = new Application[RoutingState](routingRegistry, viewPresenterRegistry, RootState)$FrontendContextPlaceholder
+ |}
+ |
+ |object Init extends JSApp with StrictLogging {
+ | import Context._
+ |
+ | @JSExport
+ | override def main(): Unit = {
+ | jQ(document).ready((_: Element) => {
+ | val appRoot = jQ("#${settings.htmlRootId}").get(0)
+ | if (appRoot.isEmpty) {
+ | logger.error("Application root element not found! Check you index.html file!")
+ | } else {
+ | applicationInstance.run(appRoot.get)$FrontendAppInitPlaceholder
+ | }
+ | })
+ | }
+ |}
+ |""".stripMargin
+ )
+
+ writeFile(statesScala)(
+ s"""package ${settings.rootPackage.mkPackage()}
+ |
+ |import io.udash._
+ |
+ |sealed abstract class RoutingState(val parentState: RoutingState) extends State {
+ | def url(implicit application: Application[RoutingState]): String = s"#${"${application.matchState(this).value}"}"
+ |}
+ |
+ |case object RootState extends RoutingState(null)
+ |
+ |case object ErrorState extends RoutingState(RootState)
+ |
+ |case object IndexState extends RoutingState(RootState)$FrontendStatesRegistryPlaceholder
+ |""".stripMargin
+ )
+
+ writeFile(routingRegistryDefScala)(
+ s"""package ${settings.rootPackage.mkPackage()}
+ |
+ |import io.udash._
+ |import io.udash.utils.Bidirectional
+ |
+ |class RoutingRegistryDef extends RoutingRegistry[RoutingState] {
+ | def matchUrl(url: Url): RoutingState =
+ | url2State.applyOrElse(url.value.stripSuffix("/"), (x: String) => ErrorState)
+ |
+ | def matchState(state: RoutingState): Url =
+ | Url(state2Url.apply(state))
+ |
+ | private val (url2State, state2Url) = Bidirectional[String, RoutingState] {
+ | case "" => IndexState$FrontendRoutingRegistryPlaceholder
+ | }
+ |}
+ |""".stripMargin
+ )
+
+ writeFile(statesToViewPresenterDefScala)(
+ s"""package ${settings.rootPackage.mkPackage()}
+ |
+ |import io.udash._
+ |import ${settings.rootPackage.mkPackage()}.${settings.viewsSubPackage.mkPackage()}._
+ |
+ |class StatesToViewPresenterDef extends ViewPresenterRegistry[RoutingState] {
+ | def matchStateToResolver(state: RoutingState): ViewPresenter[_ <: RoutingState] = state match {
+ | case RootState => RootViewPresenter
+ | case IndexState => IndexViewPresenter$FrontendVPRegistryPlaceholder
+ | case _ => ErrorViewPresenter
+ | }
+ |}
+ |""".stripMargin
+ )
+
+ writeFile(rootViewScala)(
+ s"""package ${settings.rootPackage.mkPackage()}.${settings.viewsSubPackage.mkPackage()}
+ |
+ |import io.udash._
+ |import ${settings.rootPackage.mkPackage()}.{RootState, IndexState}
+ |import org.scalajs.dom.Element
+ |
+ |object RootViewPresenter extends DefaultViewPresenterFactory[RootState.type](() => new RootView)
+ |
+ |class RootView extends View {
+ | import ${settings.rootPackage.mkPackage()}.Context._
+ | import scalatags.JsDom.all._
+ |
+ | private var child: Element = div().render
+ |
+ | private val content = div(
+ | a(href := IndexState.url)(h1("${settings.projectName}")),
+ | child
+ | ).render
+ |
+ | override def getTemplate: Element = content
+ |
+ | override def renderChild(view: View): Unit = {
+ | import io.udash.wrappers.jquery._
+ | val newChild = view.getTemplate
+ | jQ(child).replaceWith(newChild)
+ | child = newChild
+ | }
+ |}
+ |""".stripMargin
+ )
+
+ writeFile(indexViewScala)(
+ s"""package ${settings.rootPackage.mkPackage()}.${settings.viewsSubPackage.mkPackage()}
+ |
+ |import io.udash._
+ |import ${settings.rootPackage.mkPackage()}._
+ |import org.scalajs.dom.Element
+ |
+ |object IndexViewPresenter extends DefaultViewPresenterFactory[IndexState.type](() => new IndexView)
+ |
+ |class IndexView extends View {
+ | import ${settings.rootPackage.mkPackage()}.Context._
+ | import scalatags.JsDom.all._
+ |
+ | private val content = div(
+ | "Thank you for choosing Udash! Take a look at following demo pages:",
+ | ul(
+ | li(a(href := IndexState.url)("Index"))$FrontendIndexMenuPlaceholder
+ | )
+ | ).render
+ |
+ | override def getTemplate: Element = content
+ |
+ | override def renderChild(view: View): Unit = {}
+ |}
+ |""".stripMargin
+ )
+
+ writeFile(errorViewScala)(
+ s"""package ${settings.rootPackage.mkPackage()}.${settings.viewsSubPackage.mkPackage()}
+ |
+ |import io.udash._
+ |import ${settings.rootPackage.mkPackage()}.IndexState
+ |import org.scalajs.dom.Element
+ |
+ |object ErrorViewPresenter extends DefaultViewPresenterFactory[IndexState.type](() => new ErrorView)
+ |
+ |class ErrorView extends View {
+ | import scalatags.JsDom.all._
+ |
+ | private val content = h3(
+ | "URL not found!"
+ | ).render
+ |
+ | override def getTemplate: Element = content
+ |
+ | override def renderChild(view: View): Unit = {}
+ |}
+ |""".stripMargin
+ )
+ }
+}
diff --git a/core/src/main/scala/io/udash/generator/plugins/jetty/JettyLauncherPlugin.scala b/core/src/main/scala/io/udash/generator/plugins/jetty/JettyLauncherPlugin.scala
new file mode 100644
index 0000000..03de03b
--- /dev/null
+++ b/core/src/main/scala/io/udash/generator/plugins/jetty/JettyLauncherPlugin.scala
@@ -0,0 +1,124 @@
+package io.udash.generator.plugins.jetty
+
+import java.io.File
+
+import io.udash.generator.exceptions.InvalidConfiguration
+import io.udash.generator.plugins._
+import io.udash.generator.plugins.sbt.SBTProjectFiles
+import io.udash.generator.plugins.utils.{FrontendPaths, UtilPaths}
+import io.udash.generator.utils._
+import io.udash.generator.{FrontendOnlyProject, GeneratorPlugin, GeneratorSettings, StandardProject}
+
+object JettyLauncherPlugin extends GeneratorPlugin with SBTProjectFiles with FrontendPaths with UtilPaths {
+ override def run(settings: GeneratorSettings): GeneratorSettings = {
+ settings.projectType match {
+ case FrontendOnlyProject =>
+ throw InvalidConfiguration("You can not add Jetty launcher into frontend only project.")
+ case StandardProject(backend, _, frontend) =>
+ updateSBTConfig(settings, frontend)
+ createJettyServer(rootPackageInSrc(settings.rootDirectory.subFile(backend), settings), settings, backend)
+ }
+
+ settings
+ }
+
+ private def updateSBTConfig(settings: GeneratorSettings, frontendModuleName: String): Unit = {
+ val sbtConfigFile = buildSbt(settings)
+ val sbtDepsFile = dependenciesScala(settings)
+ val udashBuildFile = udashBuildScala(settings)
+
+ requireFilesExist(Seq(sbtConfigFile, sbtDepsFile, udashBuildFile))
+
+ appendOnPlaceholder(sbtConfigFile)(RootSettingsPlaceholder,
+ s""",
+ | mainClass in Compile := Some("${settings.rootPackage.mkPackage()}.Launcher")""".stripMargin)
+
+ appendOnPlaceholder(sbtConfigFile)(BackendSettingsPlaceholder,
+ s""",
+ |
+ | compile <<= (compile in Compile),
+ | (compile in Compile) <<= (compile in Compile).dependsOn(copyStatics),
+ | copyStatics := IO.copyDirectory((crossTarget in $frontendModuleName).value / StaticFilesDir, (target in Compile).value / StaticFilesDir),
+ | copyStatics <<= copyStatics.dependsOn(compileStatics in $frontendModuleName),
+ |
+ | mappings in (Compile, packageBin) ++= {
+ | copyStatics.value
+ | ((target in Compile).value / StaticFilesDir).***.get map { file =>
+ | file -> file.getAbsolutePath.stripPrefix((target in Compile).value.getAbsolutePath)
+ | }
+ | },
+ |
+ | watchSources ++= (sourceDirectory in $frontendModuleName).value.***.get""".stripMargin)
+
+ appendOnPlaceholder(sbtDepsFile)(DependenciesVariablesPlaceholder,
+ s"""
+ | val jettyVersion = "${settings.jettyVersion}"""".stripMargin)
+
+ appendOnPlaceholder(sbtDepsFile)(DependenciesBackendPlaceholder,
+ s""",
+ | "org.eclipse.jetty" % "jetty-server" % jettyVersion,
+ | "org.eclipse.jetty" % "jetty-servlet" % jettyVersion""".stripMargin)
+
+ appendOnPlaceholder(udashBuildFile)(UdashBuildPlaceholder,
+ s"""
+ | val copyStatics = taskKey[Unit]("Copy frontend static files into backend target.")""".stripMargin)
+ }
+
+ private def createJettyServer(rootPackage: File, settings: GeneratorSettings, backendModuleName: String): Unit = {
+ val jettyDir = "jetty"
+ val jettyPackage = rootPackage.subFile(jettyDir)
+ val appServerScala = jettyPackage.subFile("ApplicationServer.scala")
+ val launcherScala = rootPackage.subFile("Launcher.scala")
+
+ requireFilesExist(Seq(rootPackage))
+ createDirs(Seq(jettyPackage))
+ createFiles(Seq(appServerScala, launcherScala))
+
+ writeFile(appServerScala)(
+ s"""package ${settings.rootPackage.mkPackage()}.$jettyDir
+ |
+ |import org.eclipse.jetty.server.Server
+ |import org.eclipse.jetty.server.handler.gzip.GzipHandler
+ |import org.eclipse.jetty.server.session.SessionHandler
+ |import org.eclipse.jetty.servlet.{DefaultServlet, ServletContextHandler, ServletHolder}
+ |
+ |class ApplicationServer(val port: Int, resourceBase: String) {
+ | private val server = new Server(port)
+ | private val contextHandler = new ServletContextHandler
+ |
+ | contextHandler.setSessionHandler(new SessionHandler)
+ | contextHandler.setGzipHandler(new GzipHandler)
+ | server.setHandler(contextHandler)
+ |
+ | def start() = server.start()
+ |
+ | def stop() = server.stop()
+ |
+ | private val appHolder = {
+ | val appHolder = new ServletHolder(new DefaultServlet)
+ | appHolder.setAsyncSupported(true)
+ | appHolder.setInitParameter("resourceBase", resourceBase)
+ | appHolder
+ | }
+ | contextHandler.addServlet(appHolder, "/*")$BackendAppServerPlaceholder
+ |}
+ |
+ """.stripMargin
+ )
+
+ writeFile(launcherScala)(
+ s"""package ${settings.rootPackage.mkPackage()}
+ |
+ |import ${settings.rootPackage.mkPackage()}.$jettyDir.ApplicationServer
+ |
+ |object Launcher {
+ | def main(args: Array[String]): Unit = {
+ | val server = new ApplicationServer(8080, "$backendModuleName/target/UdashStatic/WebContent")
+ | server.start()
+ | }
+ |}
+ |
+ """.stripMargin
+ )
+ }
+}
diff --git a/core/src/main/scala/io/udash/generator/plugins/rpc/RPCDemosPlugin.scala b/core/src/main/scala/io/udash/generator/plugins/rpc/RPCDemosPlugin.scala
new file mode 100644
index 0000000..073babc
--- /dev/null
+++ b/core/src/main/scala/io/udash/generator/plugins/rpc/RPCDemosPlugin.scala
@@ -0,0 +1,106 @@
+package io.udash.generator.plugins.rpc
+
+import java.io.File
+
+import io.udash.generator.exceptions.InvalidConfiguration
+import io.udash.generator.plugins._
+import io.udash.generator.plugins.sbt.SBTProjectFiles
+import io.udash.generator.plugins.utils.{FrontendPaths, UtilPaths}
+import io.udash.generator.utils._
+import io.udash.generator.{FrontendOnlyProject, GeneratorPlugin, GeneratorSettings, StandardProject}
+
+object RPCDemosPlugin extends GeneratorPlugin with SBTProjectFiles with FrontendPaths with UtilPaths {
+ override def run(settings: GeneratorSettings): GeneratorSettings = {
+ val rootPck: File = settings.projectType match {
+ case FrontendOnlyProject =>
+ throw InvalidConfiguration("You can not add RPC into frontend only project.")
+ case StandardProject(_, shared, frontend) =>
+ rootPackageInSrc(settings.rootDirectory.subFile(frontend), settings)
+ }
+ val stateName = createDemoView(rootPck, settings)
+ addIndexLink(rootPck, stateName)
+
+ settings
+ }
+
+ private def addIndexLink(rootPackage: File, state: String): Unit = {
+ val indexViewScala = viewsPackageInSrc(rootPackage).subFile("IndexView.scala")
+ requireFilesExist(Seq(indexViewScala))
+
+ appendOnPlaceholder(indexViewScala)(FrontendIndexMenuPlaceholder,
+ s""",
+ | li(a(href := $state.url)("RPC demo"))""".stripMargin)
+ }
+
+ private def createDemoView(rootPackage: File, settings: GeneratorSettings): String = {
+ val statesScala = rootPackage.subFile("states.scala")
+ val routingRegistryDefScala = rootPackage.subFile("RoutingRegistryDef.scala")
+ val statesToViewPresenterDefScala = rootPackage.subFile("StatesToViewPresenterDef.scala")
+
+ requireFilesExist(Seq(viewsPackageInSrc(rootPackage), statesScala, routingRegistryDefScala, statesToViewPresenterDefScala))
+
+ val rpcDemoViewScala = viewsPackageInSrc(rootPackage).subFile("RPCDemoView.scala")
+ val stateName = "RPCDemoState"
+
+ appendOnPlaceholder(statesScala)(FrontendStatesRegistryPlaceholder,
+ s"""
+ |
+ |case object $stateName extends RoutingState(RootState)""".stripMargin)
+
+ appendOnPlaceholder(routingRegistryDefScala)(FrontendRoutingRegistryPlaceholder,
+ s"""
+ | case "/rpc" => $stateName""".stripMargin)
+
+ appendOnPlaceholder(statesToViewPresenterDefScala)(FrontendVPRegistryPlaceholder,
+ s"""
+ | case $stateName => RPCDemoViewPresenter""".stripMargin)
+
+ writeFile(rpcDemoViewScala)(
+ s"""package ${settings.rootPackage.mkPackage()}.${settings.viewsSubPackage.mkPackage()}
+ |
+ |import io.udash._
+ |import ${settings.rootPackage.mkPackage()}.$stateName
+ |import org.scalajs.dom.Element
+ |
+ |import scala.util.{Success, Failure}
+ |
+ |case object RPCDemoViewPresenter extends DefaultViewPresenterFactory[$stateName.type](() => {
+ | import ${settings.rootPackage.mkPackage()}.Context._
+ |
+ | val serverResponse = Property[String]("???")
+ | val input = Property[String]("")
+ | input.listen((value: String) => {
+ | serverRpc.hello(value).onComplete {
+ | case Success(resp) => serverResponse.set(resp)
+ | case Failure(_) => serverResponse.set("Error")
+ | }
+ | })
+ |
+ | serverRpc.pushMe()
+ |
+ | new RPCDemoView(input, serverResponse)
+ |})
+ |
+ |class RPCDemoView(input: Property[String], serverResponse: Property[String]) extends View {
+ | import scalatags.JsDom.all._
+ |
+ | private val content = div(
+ | div(
+ | "You can find this demo source code in: ",
+ | i("${settings.rootPackage.mkPackage()}.${settings.viewsSubPackage.mkPackage()}.RPCDemoView")
+ | ),
+ | h3("Example"),
+ | TextInput(input, placeholder := "Type your name..."),
+ | div("Server response: ", bind(serverResponse))
+ | ).render
+ |
+ | override def getTemplate: Element = content
+ |
+ | override def renderChild(view: View): Unit = {}
+ |}
+ |""".stripMargin
+ )
+
+ stateName
+ }
+}
diff --git a/core/src/main/scala/io/udash/generator/plugins/rpc/RPCPlugin.scala b/core/src/main/scala/io/udash/generator/plugins/rpc/RPCPlugin.scala
new file mode 100644
index 0000000..3fa3e04
--- /dev/null
+++ b/core/src/main/scala/io/udash/generator/plugins/rpc/RPCPlugin.scala
@@ -0,0 +1,185 @@
+package io.udash.generator.plugins.rpc
+
+import java.io.File
+
+import io.udash.generator.exceptions.InvalidConfiguration
+import io.udash.generator.plugins._
+import io.udash.generator.plugins.sbt.SBTProjectFiles
+import io.udash.generator.plugins.utils.{FrontendPaths, UtilPaths}
+import io.udash.generator.utils._
+import io.udash.generator.{FrontendOnlyProject, GeneratorPlugin, GeneratorSettings, StandardProject}
+
+object RPCPlugin extends GeneratorPlugin with SBTProjectFiles with FrontendPaths with UtilPaths {
+ val rpcDir = "rpc"
+
+ override def run(settings: GeneratorSettings): GeneratorSettings = {
+ settings.projectType match {
+ case FrontendOnlyProject =>
+ throw InvalidConfiguration("You can not add RPC into frontend only project.")
+ case StandardProject(backend, shared, frontend) =>
+ updateSBTConfig(settings)
+ createRPCInterfaces(rootPackageInSrc(settings.rootDirectory.subFile(shared), settings), settings)
+ createBackendImplementation(rootPackageInSrc(settings.rootDirectory.subFile(backend), settings), settings)
+ createFrontendImplementation(rootPackageInSrc(settings.rootDirectory.subFile(frontend), settings), settings)
+ }
+
+ settings
+ }
+
+ private def updateSBTConfig(settings: GeneratorSettings): Unit = {
+ val sbtDepsFile = dependenciesScala(settings)
+
+ requireFilesExist(Seq(sbtDepsFile))
+
+ appendOnPlaceholder(sbtDepsFile)(DependenciesVariablesPlaceholder,
+ s"""
+ | val udashRPCVersion = "${settings.udashRPCVersion}"""".stripMargin)
+
+ appendOnPlaceholder(sbtDepsFile)(DependenciesFrontendPlaceholder,
+ s""",
+ | "io.udash" %%% "udash-rpc-frontend" % udashRPCVersion""".stripMargin)
+
+ appendOnPlaceholder(sbtDepsFile)(DependenciesCrossPlaceholder,
+ s""",
+ | "io.udash" % "udash-rpc-shared" % udashRPCVersion""".stripMargin)
+
+ appendOnPlaceholder(sbtDepsFile)(DependenciesBackendPlaceholder,
+ s""",
+ | "io.udash" % "udash-rpc-backend" % udashRPCVersion,
+ | "org.eclipse.jetty.websocket" % "websocket-server" % jettyVersion""".stripMargin)
+ }
+
+ private def createRPCInterfaces(rootPackage: File, settings: GeneratorSettings): Unit = {
+ val rpcPackage = rootPackage.subFile(rpcDir)
+ val clientRPCScala = rpcPackage.subFile("MainClientRPC.scala")
+ val serverRPCScala = rpcPackage.subFile("MainServerRPC.scala")
+
+ requireFilesExist(Seq(rootPackage))
+ createDirs(Seq(rpcPackage))
+ createFiles(Seq(clientRPCScala, serverRPCScala))
+
+ writeFile(clientRPCScala)(
+ s"""package ${settings.rootPackage.mkPackage()}.$rpcDir
+ |
+ |import io.udash.rpc._
+ |
+ |trait MainClientRPC extends ClientRPC {
+ | def push(number: Int): Unit
+ |}
+ |
+ """.stripMargin
+ )
+
+ writeFile(serverRPCScala)(
+ s"""package ${settings.rootPackage.mkPackage()}.$rpcDir
+ |
+ |import io.udash.rpc._
+ |import scala.concurrent.Future
+ |
+ |trait MainServerRPC extends RPC {
+ | def hello(name: String): Future[String]
+ | def pushMe(): Unit
+ |}
+ |
+ """.stripMargin
+ )
+ }
+
+ private def createBackendImplementation(rootPackage: File, settings: GeneratorSettings): Unit = {
+ val jettyDir = "jetty"
+ val jettyPackage = rootPackage.subFile(jettyDir)
+ val rpcPackage = rootPackage.subFile(rpcDir)
+ val appServerScala = jettyPackage.subFile("ApplicationServer.scala")
+ val exposedRpcInterfacesScala = rpcPackage.subFile("ExposedRpcInterfaces.scala")
+ val clientRPCScala = rpcPackage.subFile("ClientRPC.scala")
+
+ requireFilesExist(Seq(rootPackage, jettyPackage, appServerScala))
+ createDirs(Seq(rpcPackage))
+ createFiles(Seq(clientRPCScala, exposedRpcInterfacesScala))
+
+ writeFile(clientRPCScala)(
+ s"""package ${settings.rootPackage.mkPackage()}.$rpcDir
+ |
+ |import io.udash.rpc._
+ |
+ |import scala.concurrent.ExecutionContext
+ |
+ |object ClientRPC {
+ | def apply(target: ClientRPCTarget)(implicit ec: ExecutionContext): MainClientRPC = {
+ | new DefaultClientRPC(target, AsRealRPC[MainClientRPC]).get
+ | }
+ |}
+ |
+ """.stripMargin
+ )
+
+ writeFile(exposedRpcInterfacesScala)(
+ s"""package ${settings.rootPackage.mkPackage()}.$rpcDir
+ |
+ |import io.udash.rpc._
+ |
+ |import scala.concurrent.Future
+ |import scala.concurrent.ExecutionContext.Implicits.global
+ |
+ |class ExposedRpcInterfaces(implicit clientId: ClientId) extends MainServerRPC {
+ | override def hello(name: String): Future[String] =
+ | Future.successful(s"Hello, ${"$name"}!")
+ |
+ | override def pushMe(): Unit =
+ | ClientRPC(clientId).push(42)
+ |}
+ |
+ """.stripMargin
+ )
+
+ appendOnPlaceholder(appServerScala)(BackendAppServerPlaceholder,
+ s"""
+ |
+ | private val atmosphereHolder = {
+ | import io.udash.rpc._
+ | import ${settings.rootPackage.mkPackage()}.$rpcDir._
+ |
+ | val config = new DefaultAtmosphereServiceConfig[MainServerRPC]((clientId) => new ExposesServerRPC[MainServerRPC](new ExposedRpcInterfaces()(clientId)))
+ | val framework = new DefaultAtmosphereFramework(config)
+ |
+ | framework.init()
+ |
+ | val atmosphereHolder = new ServletHolder(new RpcServlet(framework))
+ | atmosphereHolder.setAsyncSupported(true)
+ | atmosphereHolder
+ | }
+ | contextHandler.addServlet(atmosphereHolder, "/atm/*")
+ """.stripMargin
+ )
+ }
+
+ private def createFrontendImplementation(rootPackage: File, settings: GeneratorSettings): Unit = {
+ val rpcPackage = rootPackage.subFile(rpcDir)
+ val rpcServiceScala = rpcPackage.subFile("RPCService.scala")
+ val initScala = rootPackage.subFile("init.scala")
+
+ requireFilesExist(Seq(rootPackage, initScala))
+ createDirs(Seq(rpcPackage))
+ createFiles(Seq(rpcServiceScala))
+
+ writeFile(rpcServiceScala)(
+ s"""package ${settings.rootPackage.mkPackage()}.$rpcDir
+ |
+ |class RPCService extends MainClientRPC {
+ | override def push(number: Int): Unit =
+ | println(s"Push from server: ${"$number"}")
+ |}
+ |
+ """.stripMargin
+ )
+
+ appendOnPlaceholder(initScala)(FrontendContextPlaceholder,
+ s"""
+ |
+ | import io.udash.rpc._
+ | import ${settings.rootPackage.mkPackage()}.$rpcDir._
+ | val serverRpc = DefaultServerRPC[MainClientRPC, MainServerRPC](new RPCService)
+ """.stripMargin
+ )
+ }
+}
diff --git a/core/src/main/scala/io/udash/generator/plugins/sbt/SBTBootstrapPlugin.scala b/core/src/main/scala/io/udash/generator/plugins/sbt/SBTBootstrapPlugin.scala
new file mode 100644
index 0000000..f0b3d13
--- /dev/null
+++ b/core/src/main/scala/io/udash/generator/plugins/sbt/SBTBootstrapPlugin.scala
@@ -0,0 +1,71 @@
+package io.udash.generator.plugins.sbt
+
+import io.udash.generator.plugins.{DependenciesPlaceholder, UdashBuildPlaceholder}
+import io.udash.generator.utils._
+import io.udash.generator.{GeneratorPlugin, GeneratorSettings}
+
+/**
+ * Creates basic SBT project:
+ * * build.sbt with basic project settings
+ * * project/build.properties with SBT version
+ * * project/plugins.sbt with ScalaJS plugin
+ * * project/UdashBuild.scala with custom tasks
+ * * project/Dependencies.scala with dependencies
+ */
+object SBTBootstrapPlugin extends GeneratorPlugin with SBTProjectFiles {
+ override def run(settings: GeneratorSettings): GeneratorSettings = {
+ createDirs(Seq(projectDir(settings)), requireNotExists = true)
+ createFiles(Seq(buildSbt(settings), buildProperties(settings), pluginsSbt(settings),
+ udashBuildScala(settings), dependenciesScala(settings)))
+
+ writeFile(buildSbt(settings))(
+ s"""name := "${settings.projectName}"
+ |
+ |version in ThisBuild := "0.1.0-SNAPSHOT"
+ |scalaVersion in ThisBuild := "${settings.scalaVersion}"
+ |organization in ThisBuild := "${settings.organization}"
+ |crossPaths in ThisBuild := false
+ |scalacOptions in ThisBuild ++= Seq(
+ | "-feature",
+ | "-deprecation",
+ | "-unchecked",
+ | "-language:implicitConversions",
+ | "-language:existentials",
+ | "-language:dynamics",
+ | "-Xfuture",
+ | "-Xfatal-warnings",
+ | "-Xlint:_,-missing-interpolator,-adapted-args"
+ |)
+ |
+ |""".stripMargin
+ )
+
+ writeFile(buildProperties(settings))(
+ s"sbt.version = ${settings.sbtVersion}")
+
+ writeFile(pluginsSbt(settings))(
+ s"""logLevel := Level.Warn
+ |addSbtPlugin("org.scala-js" % "sbt-scalajs" % "${settings.scalaJSVersion}")
+ |
+ |""".stripMargin)
+
+ writeFile(udashBuildScala(settings))(
+ s"""import org.scalajs.sbtplugin.ScalaJSPlugin.AutoImport._
+ |import sbt.Keys._
+ |import sbt._
+ |
+ |object UdashBuild extends Build {$UdashBuildPlaceholder}
+ |
+ |""".stripMargin)
+
+ writeFile(dependenciesScala(settings))(
+ s"""import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
+ |import sbt._
+ |
+ |object Dependencies extends Build {$DependenciesPlaceholder}
+ |
+ |""".stripMargin)
+
+ settings
+ }
+}
diff --git a/core/src/main/scala/io/udash/generator/plugins/sbt/SBTModulesPlugin.scala b/core/src/main/scala/io/udash/generator/plugins/sbt/SBTModulesPlugin.scala
new file mode 100644
index 0000000..7e9e3ed
--- /dev/null
+++ b/core/src/main/scala/io/udash/generator/plugins/sbt/SBTModulesPlugin.scala
@@ -0,0 +1,293 @@
+package io.udash.generator.plugins.sbt
+
+import java.io.File
+
+import io.udash.generator.exceptions.FileCreationError
+import io.udash.generator.plugins._
+import io.udash.generator.plugins.utils.{FrontendPaths, UtilPaths}
+import io.udash.generator.utils._
+import io.udash.generator.{FrontendOnlyProject, GeneratorPlugin, GeneratorSettings, StandardProject}
+
+/**
+ * Prepares SBT modules configuration.
+ */
+object SBTModulesPlugin extends GeneratorPlugin with SBTProjectFiles with FrontendPaths with UtilPaths {
+ override def run(settings: GeneratorSettings): GeneratorSettings = {
+ settings.projectType match {
+ case FrontendOnlyProject =>
+ generateFrontendOnlyProject(settings)
+ case StandardProject(backend, shared, frontend) =>
+ generateStandardProject(
+ settings.rootDirectory.subFile(backend),
+ settings.rootDirectory.subFile(shared),
+ settings.rootDirectory.subFile(frontend), settings)
+ }
+
+ settings
+ }
+
+ /**
+ * Creates modules dirs:
+ * * src/main/assets
+ * * src/main/assets/fonts
+ * * src/main/assets/images
+ * * src/main/assets/index.dev.html
+ * * src/main/assets/index.prod.html
+ * * src/main/scala/{rootPackage}
+ * * src/test/scala/{rootPackage}
+ * and appends `build.sbt` modules config with dependencies config in `project/Dependencies.scala`.
+ */
+ private def generateFrontendOnlyProject(settings: GeneratorSettings): Unit = {
+ createModulesDirs(Seq(settings.rootDirectory), settings)
+ createFrontendExtraDirs(settings.rootDirectory, settings)
+
+ requireFilesExist(Seq(buildSbt(settings), projectDir(settings), udashBuildScala(settings), dependenciesScala(settings)))
+ generateFrontendTasks(udashBuildScala(settings), indexDevHtml(settings.rootDirectory), indexProdHtml(settings.rootDirectory))
+
+ val frontendModuleName = wrapValName(settings.projectName)
+ val depsName = wrapValName("deps")
+ val depsJSName = wrapValName("depsJS")
+
+ appendFile(buildSbt(settings))(
+ s"""val $frontendModuleName = project.in(file(".")).enablePlugins(ScalaJSPlugin)
+ | .settings(
+ | libraryDependencies ++= $depsName.value,
+ | jsDependencies ++= $depsJSName.value,
+ | persistLauncher in Compile := true,
+ |
+ | compile <<= (compile in Compile).dependsOn(compileStatics),
+ | compileStatics := {
+ | IO.copyDirectory(sourceDirectory.value / "main/assets/fonts", crossTarget.value / StaticFilesDir / WebContent / "assets/fonts")
+ | IO.copyDirectory(sourceDirectory.value / "main/assets/images", crossTarget.value / StaticFilesDir / WebContent / "assets/images")
+ | compileStaticsForRelease.value
+ | (crossTarget.value / StaticFilesDir).***.get
+ | },
+ |
+ | artifactPath in(Compile, fastOptJS) :=
+ | (crossTarget in(Compile, fastOptJS)).value / StaticFilesDir / WebContent / "scripts" / "${settings.frontendImplFastJs}",
+ | artifactPath in(Compile, fullOptJS) :=
+ | (crossTarget in(Compile, fullOptJS)).value / StaticFilesDir / WebContent / "scripts" / "${settings.frontendImplJs}",
+ | artifactPath in(Compile, packageJSDependencies) :=
+ | (crossTarget in(Compile, packageJSDependencies)).value / StaticFilesDir / WebContent / "scripts" / "${settings.frontendDepsFastJs}",
+ | artifactPath in(Compile, packageMinifiedJSDependencies) :=
+ | (crossTarget in(Compile, packageMinifiedJSDependencies)).value / StaticFilesDir / WebContent / "scripts" / "${settings.frontendDepsJs}",
+ | artifactPath in(Compile, packageScalaJSLauncher) :=
+ | (crossTarget in(Compile, packageScalaJSLauncher)).value / StaticFilesDir / WebContent / "scripts" / "${settings.frontendInitJs}"$FrontendSettingsPlaceholder
+ | )$FrontendModulePlaceholder
+ |
+ |""".stripMargin)
+
+ appendOnPlaceholder(dependenciesScala(settings))(DependenciesPlaceholder,
+ s"""
+ | $DependenciesVariablesPlaceholder
+ |
+ | val $depsName = Def.setting(Seq[ModuleID]($DependenciesFrontendPlaceholder
+ | ))
+ |
+ | val $depsJSName = Def.setting(Seq[org.scalajs.sbtplugin.JSModuleID]($DependenciesFrontendJSPlaceholder
+ | ))
+ |""".stripMargin
+ )
+ }
+
+ /**
+ * Creates modules dirs:
+ * * {module}/src/main/scala/{rootPackage}
+ * * {module}/src/test/scala/{rootPackage}
+ * extra in frontend:
+ * * {module}/src/main/assets
+ * * {module}/src/main/assets/fonts
+ * * {module}/src/main/assets/images
+ * * {module}/src/main/assets/index.dev.html
+ * * {module}/src/main/assets/index.prod.html
+ * and appends `build.sbt` modules config with dependencies config in `project/Dependencies.scala`.
+ */
+ private def generateStandardProject(backend: File, shared: File, frontend: File, settings: GeneratorSettings): Unit = {
+ createModulesDirs(Seq(backend, shared, frontend), settings)
+ createFrontendExtraDirs(frontend, settings)
+
+ requireFilesExist(Seq(buildSbt(settings), projectDir(settings), udashBuildScala(settings), dependenciesScala(settings)))
+ generateFrontendTasks(udashBuildScala(settings), indexDevHtml(frontend), indexProdHtml(frontend))
+
+ val rootModuleName = wrapValName(settings.projectName)
+ val backendModuleName = wrapValName(backend.getName)
+ val frontendModuleName = wrapValName(frontend.getName)
+ val sharedModuleName = wrapValName(shared.getName)
+ val sharedJSModuleName = wrapValName(shared.getName + "JS")
+ val sharedJVMModuleName = wrapValName(shared.getName + "JVM")
+
+ val crossDepsName = wrapValName("crossDeps")
+ val backendDepsName = wrapValName("backendDeps")
+ val frontendDepsName = wrapValName("frontendDeps")
+ val frontendJSDepsName = wrapValName("frontendJSDeps")
+
+ appendFile(buildSbt(settings))(
+ s"""def crossLibs(configuration: Configuration) =
+ | libraryDependencies ++= $crossDepsName.value.map(_ % configuration)
+ |
+ |lazy val $rootModuleName = project.in(file("."))
+ | .aggregate($sharedJSModuleName, $sharedJVMModuleName, $frontendModuleName, $backendModuleName)
+ | .dependsOn($backendModuleName)
+ | .settings(
+ | publishArtifact := false$RootSettingsPlaceholder
+ | )$RootModulePlaceholder
+ |
+ |lazy val $sharedModuleName = crossProject.crossType(CrossType.Pure).in(file("${shared.getName}"))
+ | .settings(
+ | crossLibs(Provided)$SharedSettingsPlaceholder
+ | )$SharedModulePlaceholder
+ |
+ |lazy val $sharedJVMModuleName = $sharedModuleName.jvm$SharedJVMModulePlaceholder
+ |lazy val $sharedJSModuleName = $sharedModuleName.js$SharedJSModulePlaceholder
+ |
+ |lazy val $backendModuleName = project.in(file("${backend.getName}"))
+ | .dependsOn($sharedJVMModuleName)
+ | .settings(
+ | libraryDependencies ++= $backendDepsName.value,
+ | crossLibs(Compile)$BackendSettingsPlaceholder
+ | )$BackendModulePlaceholder
+ |
+ |lazy val $frontendModuleName = project.in(file("${frontend.getName}")).enablePlugins(ScalaJSPlugin)
+ | .dependsOn($sharedJSModuleName)
+ | .settings(
+ | libraryDependencies ++= $frontendDepsName.value,
+ | crossLibs(Compile),
+ | jsDependencies ++= $frontendJSDepsName.value,
+ | persistLauncher in Compile := true,
+ |
+ | compile <<= (compile in Compile),
+ | compileStatics := {
+ | IO.copyDirectory(sourceDirectory.value / "main/assets/fonts", crossTarget.value / StaticFilesDir / WebContent / "assets/fonts")
+ | IO.copyDirectory(sourceDirectory.value / "main/assets/images", crossTarget.value / StaticFilesDir / WebContent / "assets/images")
+ | compileStaticsForRelease.value
+ | (crossTarget.value / StaticFilesDir).***.get
+ | },
+ | compileStatics <<= compileStatics.dependsOn(compile in Compile),
+ |
+ | artifactPath in(Compile, fastOptJS) :=
+ | (crossTarget in(Compile, fastOptJS)).value / StaticFilesDir / WebContent / "scripts" / "${settings.frontendImplFastJs}",
+ | artifactPath in(Compile, fullOptJS) :=
+ | (crossTarget in(Compile, fullOptJS)).value / StaticFilesDir / WebContent / "scripts" / "${settings.frontendImplJs}",
+ | artifactPath in(Compile, packageJSDependencies) :=
+ | (crossTarget in(Compile, packageJSDependencies)).value / StaticFilesDir / WebContent / "scripts" / "${settings.frontendDepsFastJs}",
+ | artifactPath in(Compile, packageMinifiedJSDependencies) :=
+ | (crossTarget in(Compile, packageMinifiedJSDependencies)).value / StaticFilesDir / WebContent / "scripts" / "${settings.frontendDepsJs}",
+ | artifactPath in(Compile, packageScalaJSLauncher) :=
+ | (crossTarget in(Compile, packageScalaJSLauncher)).value / StaticFilesDir / WebContent / "scripts" / "${settings.frontendInitJs}"$FrontendSettingsPlaceholder
+ | )$FrontendModulePlaceholder
+ |
+ |""".stripMargin)
+
+ appendOnPlaceholder(dependenciesScala(settings))(DependenciesPlaceholder,
+ s"""
+ | $DependenciesVariablesPlaceholder
+ |
+ | val $crossDepsName = Def.setting(Seq[ModuleID]($DependenciesCrossPlaceholder
+ | ))
+ |
+ | val $frontendDepsName = Def.setting(Seq[ModuleID]($DependenciesFrontendPlaceholder
+ | ))
+ |
+ | val $frontendJSDepsName = Def.setting(Seq[org.scalajs.sbtplugin.JSModuleID]($DependenciesFrontendJSPlaceholder
+ | ))
+ |
+ | val $backendDepsName = Def.setting(Seq[ModuleID]($DependenciesBackendPlaceholder
+ | ))
+ |""".stripMargin
+ )
+ }
+
+ private def createModulesDirs(modules: Seq[File], settings: GeneratorSettings): Unit = {
+ modules.foreach((modulePath: File) => {
+ val module = modulePath
+ if (modulePath != settings.rootDirectory && !module.mkdir()) throw FileCreationError(module.toString)
+ createDirs(Seq(rootPackageInSrc(module, settings), rootPackageInTestSrc(module, settings)))
+ })
+ }
+
+ private def createFrontendExtraDirs(frontend: File, settings: GeneratorSettings): Unit = {
+ createDirs(Seq(images(frontend), fonts(frontend)))
+
+ val indexDev: File = indexDevHtml(frontend)
+ val indexProd: File = indexProdHtml(frontend)
+
+ createFiles(Seq(indexDev, indexProd), requireNotExists = true)
+
+ writeFile(indexDev)(
+ s"""
+ |
+ |
+ |
+ | ${settings.projectName} - development
+ |
+ |
+ |
+ |
+ | $HTMLHeadPlaceholder
+ |
+ |
+ |
+ |
+ |
+ |""".stripMargin)
+
+ writeFile(indexProd)(
+ s"""
+ |
+ |
+ |
+ | ${settings.projectName}
+ |
+ |
+ |
+ |
+ | $HTMLHeadPlaceholder
+ |
+ |
+ |
+ |
+ |
+ |""".stripMargin)
+ }
+
+ private def generateFrontendTasks(udashBuildScala: File, indexDevHtml: File, indexProdHtml: File): Unit = {
+ appendOnPlaceholder(udashBuildScala)(UdashBuildPlaceholder,
+ s"""
+ | val StaticFilesDir = "UdashStatic"
+ | val WebContent = "WebContent"
+ |
+ | def copyIndex(file: File, to: File) = {
+ | val newFile = Path(to.toPath.toString + "/index.html")
+ | IO.copyFile(file, newFile.asFile)
+ | }
+ |
+ | val compileStatics = taskKey[Seq[File]]("Frontend static files manager.")
+ |
+ | val compileStaticsForRelease = Def.taskDyn {
+ | val outDir = crossTarget.value / StaticFilesDir / WebContent
+ | if (!isSnapshot.value) {
+ | Def.task {
+ | val indexFile = sourceDirectory.value / "main/assets/${indexProdHtml.getName}"
+ | copyIndex(indexFile, outDir)
+ | (fullOptJS in Compile).value
+ | (packageMinifiedJSDependencies in Compile).value
+ | (packageScalaJSLauncher in Compile).value
+ | }
+ | } else {
+ | Def.task {
+ | val indexFile = sourceDirectory.value / "main/assets/${indexDevHtml.getName}"
+ | copyIndex(indexFile, outDir)
+ | (fastOptJS in Compile).value
+ | (packageJSDependencies in Compile).value
+ | (packageScalaJSLauncher in Compile).value
+ | }
+ | }
+ | }
+ |""".stripMargin)
+ }
+
+ //TODO: wrap only when its necessary
+ private def wrapValName(name: String): String =
+ if (name.contains("-")) s"`$name`"
+ else name
+}
diff --git a/core/src/main/scala/io/udash/generator/plugins/sbt/SBTProjectFiles.scala b/core/src/main/scala/io/udash/generator/plugins/sbt/SBTProjectFiles.scala
new file mode 100644
index 0000000..7efedb9
--- /dev/null
+++ b/core/src/main/scala/io/udash/generator/plugins/sbt/SBTProjectFiles.scala
@@ -0,0 +1,13 @@
+package io.udash.generator.plugins.sbt
+
+import io.udash.generator.GeneratorSettings
+import io.udash.generator.utils._
+
+trait SBTProjectFiles {
+ def buildSbt(settings: GeneratorSettings) = settings.rootDirectory.subFile("build.sbt")
+ def projectDir(settings: GeneratorSettings) = settings.rootDirectory.subFile("project")
+ def buildProperties(settings: GeneratorSettings) = projectDir(settings).subFile("build.properties")
+ def pluginsSbt(settings: GeneratorSettings) = projectDir(settings).subFile("plugins.sbt")
+ def udashBuildScala(settings: GeneratorSettings) = projectDir(settings).subFile("UdashBuild.scala")
+ def dependenciesScala(settings: GeneratorSettings) = projectDir(settings).subFile("Dependencies.scala")
+}
diff --git a/core/src/main/scala/io/udash/generator/plugins/scalacss/ScalaCSSDemosPlugin.scala b/core/src/main/scala/io/udash/generator/plugins/scalacss/ScalaCSSDemosPlugin.scala
new file mode 100644
index 0000000..cb9036d
--- /dev/null
+++ b/core/src/main/scala/io/udash/generator/plugins/scalacss/ScalaCSSDemosPlugin.scala
@@ -0,0 +1,175 @@
+package io.udash.generator.plugins.scalacss
+
+import java.io.File
+
+import io.udash.generator.plugins._
+import io.udash.generator.plugins.sbt.SBTProjectFiles
+import io.udash.generator.plugins.utils.{FrontendPaths, UtilPaths}
+import io.udash.generator.utils._
+import io.udash.generator.{FrontendOnlyProject, GeneratorPlugin, GeneratorSettings, StandardProject}
+
+object ScalaCSSDemosPlugin extends GeneratorPlugin with SBTProjectFiles with FrontendPaths with UtilPaths {
+
+ override def run(settings: GeneratorSettings): GeneratorSettings = {
+ val rootPck: File = settings.projectType match {
+ case FrontendOnlyProject =>
+ rootPackageInSrc(settings.rootDirectory, settings)
+ case StandardProject(_, shared, frontend) =>
+ rootPackageInSrc(settings.rootDirectory.subFile(frontend), settings)
+ }
+ val stateName = createDemoStyles(rootPck, settings)
+ addIndexLink(rootPck, stateName)
+
+ settings
+ }
+
+ private def addIndexLink(rootPackage: File, state: String): Unit = {
+ val indexViewScala = viewsPackageInSrc(rootPackage).subFile("IndexView.scala")
+ requireFilesExist(Seq(indexViewScala))
+
+ appendOnPlaceholder(indexViewScala)(FrontendIndexMenuPlaceholder,
+ s""",
+ | li(a(href := $state.url)("ScalaCSS demo view"))""".stripMargin)
+ }
+
+ private def createDemoStyles(rootPackage: File, settings: GeneratorSettings): String = {
+ val demoStylesScala = stylesPackageInSrc(rootPackage).subFile("DemoStyles.scala")
+ val initScala = rootPackage.subFile("init.scala")
+
+ val statesScala = rootPackage.subFile("states.scala")
+ val routingRegistryDefScala = rootPackage.subFile("RoutingRegistryDef.scala")
+ val statesToViewPresenterDefScala = rootPackage.subFile("StatesToViewPresenterDef.scala")
+
+ val demoStylesViewScala = viewsPackageInSrc(rootPackage).subFile("DemoStylesView.scala")
+ val stateName = "DemoStylesState"
+
+ createDirs(Seq(stylesPackageInSrc(rootPackage)))
+ createFiles(Seq(demoStylesScala, demoStylesViewScala), requireNotExists = true)
+ requireFilesExist(Seq(dependenciesScala(settings), initScala, statesScala, routingRegistryDefScala, statesToViewPresenterDefScala))
+
+ appendOnPlaceholder(dependenciesScala(settings))(DependenciesFrontendPlaceholder,
+ s""",
+ | "com.github.japgolly.scalacss" %%% "core" % "${settings.scalaCSSVersion}",
+ | "com.github.japgolly.scalacss" %%% "ext-scalatags" % "${settings.scalaCSSVersion}"""".stripMargin)
+
+ appendOnPlaceholder(statesScala)(FrontendStatesRegistryPlaceholder,
+ s"""
+ |
+ |case object $stateName extends RoutingState(RootState)""".stripMargin)
+
+ appendOnPlaceholder(routingRegistryDefScala)(FrontendRoutingRegistryPlaceholder,
+ s"""
+ | case "/scalacss" => $stateName""".stripMargin)
+
+ appendOnPlaceholder(statesToViewPresenterDefScala)(FrontendVPRegistryPlaceholder,
+ s"""
+ | case $stateName => DemoStylesViewPresenter""".stripMargin)
+
+ writeFile(demoStylesViewScala)(
+ s"""package ${settings.rootPackage.mkPackage()}.${settings.viewsSubPackage.mkPackage()}
+ |
+ |import io.udash._
+ |import ${settings.rootPackage.mkPackage()}.$stateName
+ |import org.scalajs.dom.Element
+ |
+ |import scala.concurrent.duration.DurationInt
+ |import scala.language.postfixOps
+ |import scalacss.Defaults._
+ |
+ |case object DemoStylesViewPresenter extends DefaultViewPresenterFactory[$stateName.type](() => new DemoStylesView)
+ |
+ |class DemoStylesView extends View {
+ | import scalacss.Defaults._
+ | import scalacss.ScalatagsCss._
+ | import scalatags.JsDom._
+ | import scalatags.JsDom.all._
+ |
+ | private val content = div(
+ | LocalStyles.render[TypedTag[org.scalajs.dom.raw.HTMLStyleElement]],
+ | div(
+ | "You can find this demo source code in: ",
+ | i("${settings.rootPackage.mkPackage()}.${settings.viewsSubPackage.mkPackage()}.DemoStylesView")
+ | ),
+ | h3("Example"),
+ | p(LocalStyles.redItalic)("Red italic text."),
+ | p(LocalStyles.obliqueOnHover)("Hover me!"),
+ | h3("Read more"),
+ | a(href := "http://japgolly.github.io/scalacss/book/")("Read more in ScalaCSS docs.")
+ | ).render
+ |
+ | override def getTemplate: Element = content
+ |
+ | override def renderChild(view: View): Unit = {}
+ |
+ | object LocalStyles extends StyleSheet.Inline {
+ | import dsl._
+ |
+ | val redItalic = style(
+ | fontStyle.italic,
+ | color.red
+ | )
+ |
+ | val obliqueOnHover = style(
+ | fontStyle.normal,
+ |
+ | &.hover(
+ | fontStyle.oblique
+ | )
+ | )
+ | }
+ |}
+ |""".stripMargin
+ )
+
+ appendOnPlaceholder(initScala)(FrontendAppInitPlaceholder,
+ s"""
+ |
+ |import scalacss.Defaults._
+ |import scalacss.ScalatagsCss._
+ |import scalatags.JsDom._
+ |import scalatags.JsDom.all._
+ |import ${settings.rootPackage.mkPackage()}.${settings.stylesSubPackage.mkPackage()}.DemoStyles
+ |jQ(appRoot.get).addClass(DemoStyles.container.className.value)
+ |jQ(DemoStyles.render[TypedTag[org.scalajs.dom.raw.HTMLStyleElement]].render).insertBefore(appRoot.get)""".stripMargin)
+
+ writeFile(demoStylesScala)(
+ s"""package ${settings.rootPackage.mkPackage()}.${settings.stylesSubPackage.mkPackage()}
+ |
+ |import scala.concurrent.duration.DurationInt
+ |import scala.language.postfixOps
+ |import scalacss.Defaults._
+ |
+ |object DemoStyles extends StyleSheet.Inline {
+ | import dsl._
+ |
+ | val linkHoverAnimation = keyframes(
+ | (0 %%) -> keyframe(color.black),
+ | (50 %%) -> keyframe(color.red),
+ | (100 %%) -> keyframe(color.black)
+ | )
+ |
+ | val container = style(
+ | width(1000 px),
+ | margin(0 px, auto),
+ |
+ | unsafeChild("h1")(
+ | textDecorationStyle.unset,
+ | textDecorationLine.none
+ | ),
+ |
+ | unsafeChild("a")(
+ | color.black,
+ |
+ | &.hover(
+ | animationName(linkHoverAnimation),
+ | animationIterationCount.count(1),
+ | animationDuration(300 milliseconds)
+ | )
+ | )
+ | )
+ |}
+ |""".stripMargin)
+
+ stateName
+ }
+}
diff --git a/core/src/main/scala/io/udash/generator/plugins/utils/FrontendPaths.scala b/core/src/main/scala/io/udash/generator/plugins/utils/FrontendPaths.scala
new file mode 100644
index 0000000..a8889a8
--- /dev/null
+++ b/core/src/main/scala/io/udash/generator/plugins/utils/FrontendPaths.scala
@@ -0,0 +1,28 @@
+package io.udash.generator.plugins.utils
+
+import java.io.File
+
+import io.udash.generator.utils._
+
+trait FrontendPaths { utils: UtilPaths =>
+ def assets(module: File): File =
+ module.subFile(assetsPathPart)
+ def images(module: File): File =
+ module.subFile(imagesPathPart)
+ def fonts(module: File): File =
+ module.subFile(fontsPathPart)
+ def indexDevHtml(module: File): File =
+ new File(s"$module${File.separator}$assetsPathPart${File.separator}index.dev.html")
+ def indexProdHtml(module: File): File =
+ new File(s"$module${File.separator}$assetsPathPart${File.separator}index.prod.html")
+
+ def viewsPackageInSrc(rootPackage: File): File =
+ rootPackage.subFile("views")
+
+ def stylesPackageInSrc(rootPackage: File): File =
+ rootPackage.subFile("styles")
+
+ private val assetsPathPart = Seq("src", "main", "assets").mkString(File.separator)
+ private val imagesPathPart = Seq(assetsPathPart, "images").mkString(File.separator)
+ private val fontsPathPart = Seq(assetsPathPart, "fonts").mkString(File.separator)
+}
diff --git a/core/src/main/scala/io/udash/generator/plugins/utils/UtilPaths.scala b/core/src/main/scala/io/udash/generator/plugins/utils/UtilPaths.scala
new file mode 100644
index 0000000..e751cc8
--- /dev/null
+++ b/core/src/main/scala/io/udash/generator/plugins/utils/UtilPaths.scala
@@ -0,0 +1,21 @@
+package io.udash.generator.plugins.utils
+
+import java.io.File
+
+import io.udash.generator.GeneratorSettings
+import io.udash.generator.utils._
+
+trait UtilPaths {
+ def src(module: File): File =
+ module.subFile(srcPathPart)
+ def testSrc(module: File): File =
+ module.subFile(testSrcPathPart)
+ def rootPackageInSrc(module: File, settings: GeneratorSettings): File =
+ module.subFile(Seq(srcPathPart, packagePathPart(settings)).mkString(File.separator))
+ def rootPackageInTestSrc(module: File, settings: GeneratorSettings): File =
+ module.subFile(Seq(testSrcPathPart, packagePathPart(settings)).mkString(File.separator))
+
+ private val srcPathPart = Seq("src", "main", "scala").mkString(File.separator)
+ private val testSrcPathPart = Seq("src", "test", "scala").mkString(File.separator)
+ private def packagePathPart(settings: GeneratorSettings) = settings.rootPackage.mkString(File.separator)
+}
diff --git a/core/src/main/scala/io/udash/generator/utils/FileOps.scala b/core/src/main/scala/io/udash/generator/utils/FileOps.scala
new file mode 100644
index 0000000..e40133e
--- /dev/null
+++ b/core/src/main/scala/io/udash/generator/utils/FileOps.scala
@@ -0,0 +1,56 @@
+package io.udash.generator.utils
+
+import java.io._
+
+import io.udash.generator.plugins.Placeholder
+
+import scala.io.Source
+import scala.util.matching.Regex
+
+/** Basic file operations */
+trait FileOps {
+ /** Writes `content` into `file`. */
+ protected def writeFile(file: File)(content: String) = {
+ new PrintWriter(file) {
+ write(content)
+ close()
+ }
+ }
+
+ /** Appends `content` into `file`. */
+ protected def appendFile(file: File)(content: String) = {
+ new PrintWriter(new BufferedWriter(new FileWriter(file.getAbsolutePath, true))) {
+ write(content)
+ close()
+ }
+ }
+
+ /** Replaces all parts of `file` matching `regex` with `replacement`. */
+ protected def replaceInFile(file: File)(regex: String, replacement: String) = {
+ val current: String = readWholeFile(file)
+ new PrintWriter(file) {
+ write(current.replaceAll(regex, replacement))
+ close()
+ }
+ }
+
+ /** Adds `content` before all occurrences of `placeholder` in `file`. */
+ protected def appendOnPlaceholder(file: File)(placeholder: Placeholder, content: String) =
+ replaceInFile(file)(Regex.quote(placeholder.toString), content+placeholder.toString)
+
+ /** Removes all parts of `file` matching `regex`. */
+ protected def removeFromFile(file: File)(regex: String) =
+ replaceInFile(file)(regex, "")
+
+ protected def removeFileOrDir(file: File): Unit = {
+ if (file.exists()) {
+ if (file.isDirectory) {
+ file.listFiles().foreach(removeFileOrDir)
+ }
+ file.delete()
+ }
+ }
+
+ private def readWholeFile(file: File): String =
+ Source.fromFile(file).getLines.mkString("\n")
+}
diff --git a/core/src/main/scala/io/udash/generator/utils/package.scala b/core/src/main/scala/io/udash/generator/utils/package.scala
new file mode 100644
index 0000000..1125a70
--- /dev/null
+++ b/core/src/main/scala/io/udash/generator/utils/package.scala
@@ -0,0 +1,33 @@
+package io.udash.generator
+
+import java.io.File
+
+import io.udash.generator.exceptions._
+
+package object utils {
+ def createFiles(files: Seq[File], requireNotExists: Boolean = false): Unit = {
+ files.foreach((file: File) => {
+ if (!file.createNewFile() && requireNotExists) throw FileCreationError(s"${file.getAbsolutePath} already exists!")
+ })
+ }
+
+ def createDirs(files: Seq[File], requireNotExists: Boolean = false): Unit = {
+ files.foreach((file: File) => {
+ if (!file.mkdirs() && requireNotExists) throw FileCreationError(s"${file.getAbsolutePath} already exists!")
+ })
+ }
+
+ def requireFilesExist(files: Seq[File]): Unit = {
+ files.foreach((file: File) => {
+ if (!file.exists()) throw FileDoesNotExist(s"${file.getAbsolutePath} does not exist!")
+ })
+ }
+
+ implicit class SeqOps(private val s: Seq[String]) extends AnyVal {
+ def mkPackage(): String = s.mkString(".")
+ }
+
+ implicit class FileExt(private val file: File) extends AnyVal {
+ def subFile(f: String): File = new File(s"${file.getAbsolutePath}${File.separator}$f")
+ }
+}
diff --git a/dist/run.bat b/dist/run.bat
new file mode 100755
index 0000000..428dfd5
--- /dev/null
+++ b/dist/run.bat
@@ -0,0 +1 @@
+java -jar udash-generator.jar
\ No newline at end of file
diff --git a/dist/run.sh b/dist/run.sh
new file mode 100755
index 0000000..02e66cf
--- /dev/null
+++ b/dist/run.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+java -jar udash-generator.jar
\ No newline at end of file
diff --git a/project/build.properties b/project/build.properties
new file mode 100644
index 0000000..da4bbed
--- /dev/null
+++ b/project/build.properties
@@ -0,0 +1 @@
+sbt.version = 0.13.9
\ No newline at end of file
diff --git a/project/plugins.sbt b/project/plugins.sbt
new file mode 100644
index 0000000..b91cdca
--- /dev/null
+++ b/project/plugins.sbt
@@ -0,0 +1,3 @@
+logLevel := Level.Warn
+
+addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.1")
\ No newline at end of file
diff --git a/test.sh b/test.sh
new file mode 100755
index 0000000..b9222ca
--- /dev/null
+++ b/test.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+
+echo "Assembling Udash generator..."
+sbt assembly
+cp cmd/target/scala-2.11/udash-generator.jar dist/udash-generator.jar
+
+cd dist
+
+ERRORS=0
+for f in ../test/*.cnf; do
+ echo "Starting test $f..."
+ ./run.sh < $f > /dev/null
+ cd test-app
+ sbt compile > ../$f.log
+ if [ $? -eq 0 ]; then
+ echo -e "Test $f \e[32msucceed\e[39m!"
+ else
+ echo -e "Test $f \e[31mfailed\e[39m!"
+ ((ERRORS++))
+ fi
+ cd ..
+done
+
+cd ..
+exit ${ERRORS}
\ No newline at end of file
diff --git a/test/basic-front-only.cnf b/test/basic-front-only.cnf
new file mode 100644
index 0000000..a6b4fec
--- /dev/null
+++ b/test/basic-front-only.cnf
@@ -0,0 +1,10 @@
+test-app
+true
+empty-app
+io.udash.app
+io.udash.app
+1
+true
+false
+false
+true
\ No newline at end of file
diff --git a/test/basic-front.cnf b/test/basic-front.cnf
new file mode 100644
index 0000000..89909fb
--- /dev/null
+++ b/test/basic-front.cnf
@@ -0,0 +1,14 @@
+test-app
+true
+empty-app
+io.udash.app
+io.udash.app
+2
+backend
+shared
+frontend
+true
+false
+false
+false
+true
\ No newline at end of file
diff --git a/test/demos-front-only.cnf b/test/demos-front-only.cnf
new file mode 100644
index 0000000..bc584eb
--- /dev/null
+++ b/test/demos-front-only.cnf
@@ -0,0 +1,10 @@
+test-app
+true
+empty-app
+io.udash.app
+io.udash.app
+1
+true
+true
+true
+true
\ No newline at end of file
diff --git a/test/demos-front.cnf b/test/demos-front.cnf
new file mode 100644
index 0000000..d192615
--- /dev/null
+++ b/test/demos-front.cnf
@@ -0,0 +1,14 @@
+test-app
+true
+empty-app
+io.udash.app
+io.udash.app
+2
+backend
+shared
+frontend
+true
+true
+true
+false
+true
\ No newline at end of file
diff --git a/test/empty.cnf b/test/empty.cnf
new file mode 100644
index 0000000..e40f356
--- /dev/null
+++ b/test/empty.cnf
@@ -0,0 +1,11 @@
+test-app
+true
+empty-app
+io.udash.app
+io.udash.app
+2
+backend
+shared
+frontend
+false
+true
\ No newline at end of file
diff --git a/test/full-com-example.cnf b/test/full-com-example.cnf
new file mode 100644
index 0000000..1ac9e9c
--- /dev/null
+++ b/test/full-com-example.cnf
@@ -0,0 +1,16 @@
+test-app
+true
+full-app
+com.example
+com.example.app
+2
+backend
+shared
+frontend
+true
+true
+true
+true
+true
+true
+true
\ No newline at end of file
diff --git a/test/full-custom-modules.cnf b/test/full-custom-modules.cnf
new file mode 100644
index 0000000..4506750
--- /dev/null
+++ b/test/full-custom-modules.cnf
@@ -0,0 +1,16 @@
+test-app
+true
+full-app
+io.udash.app.full
+io.udash.app.full
+2
+back
+share
+front
+true
+true
+true
+true
+true
+true
+true
\ No newline at end of file
diff --git a/test/full.cnf b/test/full.cnf
new file mode 100644
index 0000000..25ab9ed
--- /dev/null
+++ b/test/full.cnf
@@ -0,0 +1,16 @@
+test-app
+true
+full-app
+io.udash.app.full
+io.udash.app.full
+2
+backend
+shared
+frontend
+true
+true
+true
+true
+true
+true
+true
\ No newline at end of file
diff --git a/test/jetty.cnf b/test/jetty.cnf
new file mode 100644
index 0000000..b1a0bb3
--- /dev/null
+++ b/test/jetty.cnf
@@ -0,0 +1,15 @@
+test-app
+true
+empty-app
+io.udash.app
+io.udash.app
+2
+backend
+shared
+frontend
+true
+false
+false
+true
+false
+true
\ No newline at end of file
diff --git a/test/rpc.cnf b/test/rpc.cnf
new file mode 100644
index 0000000..0e8e9d5
--- /dev/null
+++ b/test/rpc.cnf
@@ -0,0 +1,16 @@
+test-app
+true
+empty-app
+io.udash.app
+io.udash.app
+2
+backend
+shared
+frontend
+true
+false
+false
+true
+true
+true
+true
\ No newline at end of file