├── .scalafmt.conf ├── .gitignore ├── shared └── src │ └── app │ └── Api.scala ├── .travis.yml ├── www └── index.html ├── jvm └── src │ └── app │ ├── ApiImpl.scala │ └── Server.scala ├── js └── src │ └── app │ ├── Client.scala │ └── Main.scala ├── README.md └── mill /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "2.3.2" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | .idea 3 | .idea_modules 4 | .metals 5 | .bloop 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /shared/src/app/Api.scala: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | trait Api[F[_]] { 4 | def increment(i: Int): F[Int] 5 | 6 | def decrement(i: Int): F[Int] 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | node_js: 3 | - 12 4 | script: 5 | - ./mill -i __.compile 6 | - ./mill -i __.fastOpt 7 | cache: 8 | directories: 9 | - $HOME/.mill 10 | - $HOME/.cache 11 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Scala Fullstack app 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /jvm/src/app/ApiImpl.scala: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import scala.concurrent.Future 4 | 5 | object ApiImpl extends Api[Future] { 6 | def increment(i: Int): Future[Int] = Future.successful(i + 1) 7 | 8 | def decrement(i: Int): Future[Int] = Future.successful(i - 1) 9 | } 10 | -------------------------------------------------------------------------------- /js/src/app/Client.scala: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import boopickle.Default._ 4 | import chameleon.ext.boopickle._ 5 | import java.nio.ByteBuffer 6 | import covenant.http.HttpClient 7 | import scala.concurrent.Future 8 | import scalajs.concurrent.JSExecutionContext.Implicits.queue 9 | 10 | 11 | object Client { 12 | val client = HttpClient[ByteBuffer]("/api") 13 | val api: Api[Future] = client.wire[Api[Future]] 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scala-fullstack 2 | 3 | Full stack Scala skeleton project using Akka-http, Scala.js, Laminar, Sloth, Boopickle. 4 | 5 | It uses the [Mill build tool](http://www.lihaoyi.com/mill/). 6 | 7 | To start the server: 8 | 9 | ```bash 10 | ./mill -watch jvm.runBackground 11 | ``` 12 | 13 | in another shell you need to generate the javascript output: 14 | 15 | ```bash 16 | ./mill -watch js.fastOpt 17 | ``` 18 | 19 | then you can open the served page at `http://localhost:8080`. 20 | -------------------------------------------------------------------------------- /js/src/app/Main.scala: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import com.raquo.laminar.api.L._ 4 | import org.scalajs.dom.document 5 | 6 | import scala.util.Try 7 | 8 | object Main { 9 | val nameInput = input( 10 | placeholder := "Insert your name" 11 | ) 12 | 13 | val nameStream = nameInput.events(onInput).mapTo(nameInput.ref.value) 14 | 15 | val incrementInput = input( 16 | placeholder := "Insert a number" 17 | ) 18 | 19 | val incrementStream = 20 | incrementInput.events(onInput).mapTo(incrementInput.ref.value) 21 | 22 | val resultStream: EventStream[Int] = incrementStream.map { s => 23 | val optionInt = Try(s.toInt).toOption 24 | val result = optionInt.map(Client.api.increment) //executed on server! 25 | result 26 | .map { f => 27 | EventStream.fromFuture(f) 28 | } 29 | .getOrElse(EventStream.fromSeq(Seq.empty, emitOnce = true)) 30 | }.flatten 31 | 32 | val app = div( 33 | nameInput, 34 | p("Hello ", child.text <-- nameStream), 35 | h3("Server implemented function:"), 36 | incrementInput, 37 | p("result from server: ", child.text <-- resultStream.map(_.toString)) 38 | ) 39 | 40 | def main(args: Array[String]): Unit = { 41 | render(document.body, app) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /jvm/src/app/Server.scala: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import java.nio.ByteBuffer 4 | 5 | import akka.actor.ActorSystem 6 | import akka.event.Logging 7 | import akka.http.scaladsl.Http 8 | import akka.http.scaladsl.model.headers.`Access-Control-Allow-Origin` 9 | import akka.http.scaladsl.server.Directives._ 10 | import akka.http.scaladsl.server.directives.DebuggingDirectives 11 | import akka.stream.ActorMaterializer 12 | import covenant.http.AkkaHttpRoute 13 | import covenant.http.ByteBufferImplicits._ 14 | import sloth.Router 15 | import boopickle.Default._ 16 | import chameleon.ext.boopickle._ 17 | import cats.implicits._ 18 | 19 | import scala.concurrent.Future 20 | 21 | object Server { 22 | implicit val system = ActorSystem("my-system") 23 | implicit val materializer = ActorMaterializer() 24 | implicit val executionContext = system.dispatcher 25 | 26 | val router = Router[ByteBuffer, Future].route[Api[Future]](ApiImpl) 27 | 28 | val apiRoute = pathPrefix("api")(AkkaHttpRoute.fromFutureRouter(router)) 29 | 30 | val filesRoutes = pathPrefix("") { 31 | getFromDirectory("www") 32 | } ~ path("") { 33 | getFromFile("www/index.html") 34 | } ~ path("index.js") { 35 | getFromFile("out/js/fastOpt/dest/out.js") 36 | } 37 | 38 | val loggedRoute = 39 | DebuggingDirectives.logRequestResult("Client Rest", Logging.InfoLevel)( 40 | filesRoutes ~ apiRoute 41 | ) 42 | 43 | def main(args: Array[String]): Unit = { 44 | Http().bindAndHandle(loggedRoute, interface = "0.0.0.0", port = 8080) 45 | 46 | println(s"Server online at http://0.0.0.0:8080/") 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /mill: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # This is a wrapper script, that automatically download mill from GitHub release pages 4 | # You can give the required mill version with MILL_VERSION env variable 5 | # If no version is given, it falls back to the value of DEFAULT_MILL_VERSION 6 | DEFAULT_MILL_VERSION=0.8.0-6-bbbf19 7 | 8 | set -e 9 | 10 | if [ -z "$MILL_VERSION" ] ; then 11 | if [ -f ".mill-version" ] ; then 12 | MILL_VERSION="$(head -n 1 .mill-version 2> /dev/null)" 13 | elif [ -f "mill" ] && [ "$BASH_SOURCE" != "mill" ] ; then 14 | MILL_VERSION=$(grep -F "DEFAULT_MILL_VERSION=" "mill" | head -n 1 | cut -d= -f2) 15 | else 16 | MILL_VERSION=$DEFAULT_MILL_VERSION 17 | fi 18 | fi 19 | 20 | if [ "x${XDG_CACHE_HOME}" != "x" ] ; then 21 | MILL_DOWNLOAD_PATH="${XDG_CACHE_HOME}/mill/download" 22 | else 23 | MILL_DOWNLOAD_PATH="${HOME}/.cache/mill/download" 24 | fi 25 | MILL_EXEC_PATH="${MILL_DOWNLOAD_PATH}/${MILL_VERSION}" 26 | 27 | version_remainder="$MILL_VERSION" 28 | MILL_MAJOR_VERSION="${version_remainder%%.*}"; version_remainder="${version_remainder#*.}" 29 | MILL_MINOR_VERSION="${version_remainder%%.*}"; version_remainder="${version_remainder#*.}" 30 | 31 | if [ ! -x "$MILL_EXEC_PATH" ] ; then 32 | mkdir -p $MILL_DOWNLOAD_PATH 33 | if [ "$MILL_MAJOR_VERSION" -gt 0 ] || [ "$MILL_MINOR_VERSION" -ge 5 ] ; then 34 | ASSEMBLY="-assembly" 35 | fi 36 | DOWNLOAD_FILE=$MILL_EXEC_PATH-tmp-download 37 | MILL_DOWNLOAD_URL="https://github.com/lihaoyi/mill/releases/download/${MILL_VERSION%%-*}/$MILL_VERSION${ASSEMBLY}" 38 | curl --fail -L -o "$DOWNLOAD_FILE" "$MILL_DOWNLOAD_URL" 39 | chmod +x "$DOWNLOAD_FILE" 40 | mv "$DOWNLOAD_FILE" "$MILL_EXEC_PATH" 41 | unset DOWNLOAD_FILE 42 | unset MILL_DOWNLOAD_URL 43 | fi 44 | 45 | unset MILL_DOWNLOAD_PATH 46 | unset MILL_VERSION 47 | 48 | exec $MILL_EXEC_PATH "$@" 49 | --------------------------------------------------------------------------------