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