Skip to content

Commit

Permalink
Merge pull request #23 from Quafadas/frontendRouting
Browse files Browse the repository at this point in the history
Might serve spa routes
  • Loading branch information
Quafadas authored May 30, 2024
2 parents 5ddda40 + c6cd977 commit 5c536f2
Show file tree
Hide file tree
Showing 9 changed files with 515 additions and 216 deletions.
19 changes: 19 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "scala",
"request": "attach",
"name": "Attach debugger",
// name of the module that is being debugging
"buildTarget": "project.test",
// Host of the jvm to connect to
"hostName": "localhost",
// Port to connect to
"port": 5005
}
]
}
14 changes: 12 additions & 2 deletions project/src/live.server.scala
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ case class CliValidationError(message: String) extends NoStackTrace

object LiveServer extends IOApp:
private val logger = scribe.cats[IO]
given filesInstance: Files[IO] = Files.forAsync[IO]

private def buildServer(httpApp: HttpApp[IO], port: Port) = EmberServerBuilder
.default[IO]
Expand Down Expand Up @@ -115,6 +116,13 @@ object LiveServer extends IOApp:
.option[String]("proxy-prefix-path", "Match routes starting with this prefix - e.g. /api")
.orNone

val clientRoutingPrefixOpt = Opts
.option[String](
"client-routes-prefix",
"Routes starting with this prefix e.g. /app will return index.html. This enables client side routing via e.g. waypoint"
)
.orNone

val buildToolOpt = Opts
.option[String]("build-tool", "scala-cli or mill")
.validate("Invalid build tool") {
Expand Down Expand Up @@ -168,6 +176,7 @@ object LiveServer extends IOApp:
portOpt,
proxyPortTargetOpt,
proxyPathMatchPrefixOpt,
clientRoutingPrefixOpt,
logLevelOpt,
buildToolOpt,
openBrowserAtOpt,
Expand All @@ -182,6 +191,7 @@ object LiveServer extends IOApp:
port,
proxyTarget,
pathPrefix,
clientRoutingPrefix,
lvl,
buildTool,
openBrowserAt,
Expand Down Expand Up @@ -259,7 +269,7 @@ object LiveServer extends IOApp:
millModuleName
)(logger)

app <- routes(outDirString, refreshTopic, indexOpts, proxyRoutes, fileToHashRef)(logger)
app <- routes(outDirString, refreshTopic, indexOpts, proxyRoutes, fileToHashRef, clientRoutingPrefix)(logger)

_ <- updateMapRef(outDirPath, fileToHashRef)(logger).toResource
// _ <- stylesDir.fold(Resource.unit)(sd => seedMapOnStart(sd, mr))
Expand All @@ -271,7 +281,7 @@ object LiveServer extends IOApp:

// _ <- stylesDir.fold(Resource.unit[IO])(sd => fileWatcher(fs2.io.file.Path(sd), mr))
_ <- logger.info(s"Start dev server on http://localhost:$port").toResource
server <- buildServer(app, port)
server <- buildServer(app.orNotFound, port)

- <- openBrowser(Some(openBrowserAt), port)(logger).toResource
yield server
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import org.http4s.Status
import org.http4s.dsl.io.*
import org.typelevel.ci.CIStringSyntax

import fs2.*

import scribe.Scribe

import cats.data.Kleisli
Expand Down
22 changes: 22 additions & 0 deletions project/src/middleware/noCache.middleware.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import org.http4s.Header
import org.http4s.HttpRoutes
import org.http4s.Request
import org.typelevel.ci.CIStringSyntax

import cats.data.Kleisli
import cats.effect.*
import cats.effect.IO

object NoCacheMiddlware:

def apply(service: HttpRoutes[IO]): HttpRoutes[IO] = Kleisli {
(req: Request[IO]) =>
service(req).map {
resp =>
resp.putHeaders(
Header.Raw(ci"Cache-Control", "no-cache")
)
}
}

end NoCacheMiddlware
36 changes: 36 additions & 0 deletions project/src/middleware/static.file.middleware.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime

import org.http4s.Header
import org.http4s.HttpRoutes
import org.http4s.Request
import org.http4s.Response
import org.http4s.Status
import org.http4s.dsl.io.*
import org.typelevel.ci.CIStringSyntax

import fs2.*
import fs2.io.file.Path

import scribe.Scribe

import cats.data.Kleisli
import cats.data.OptionT
import cats.effect.*
import cats.effect.IO
import cats.syntax.all.*

def parseFromHeader(epochInstant: Instant, header: String): Long =
java.time.Duration.between(epochInstant, ZonedDateTime.parse(header, formatter)).toSeconds()
end parseFromHeader

object StaticFileMiddleware:
def apply(service: HttpRoutes[IO], file: Path)(logger: Scribe[IO]): HttpRoutes[IO] = Kleisli {
(req: Request[IO]) =>

val epochInstant: Instant = Instant.EPOCH

cachedFileResponse(epochInstant, file, req, service)(logger: Scribe[IO])
}
end StaticFileMiddleware
92 changes: 92 additions & 0 deletions project/src/middleware/static.path.middleware.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime

import org.http4s.Header
import org.http4s.HttpRoutes
import org.http4s.Request
import org.http4s.Response
import org.http4s.Status
import org.http4s.dsl.io.*
import org.typelevel.ci.CIStringSyntax

import fs2.*
import fs2.io.file.Path

import scribe.Scribe

import cats.data.Kleisli
import cats.data.OptionT
import cats.effect.*
import cats.effect.IO
import cats.syntax.all.*

inline def respondWithCacheLastModified(resp: Response[IO], lastModZdt: ZonedDateTime) =
resp.putHeaders(
Header.Raw(ci"Cache-Control", "no-cache"),
Header.Raw(ci"ETag", lastModZdt.toInstant.getEpochSecond.toString()),
Header.Raw(
ci"Last-Modified",
formatter.format(lastModZdt)
),
Header.Raw(
ci"Expires",
httpCacheFormat(ZonedDateTime.ofInstant(Instant.now().plusSeconds(10000000), ZoneId.of("GMT")))
)
)
end respondWithCacheLastModified

inline def cachedFileResponse(epochInstant: Instant, fullPath: Path, req: Request[IO], service: HttpRoutes[IO])(
logger: Scribe[IO]
) =
OptionT
.liftF(fileLastModified(fullPath))
.flatMap {
lastmod =>
req.headers.get(ci"If-Modified-Since") match
case Some(header) =>
val browserLastModifiedAt = header.head.value
service(req).semiflatMap {
resp =>
val zdt = ZonedDateTime.ofInstant(Instant.ofEpochSecond(lastmod), ZoneId.of("GMT"))
val response =
if parseFromHeader(epochInstant, browserLastModifiedAt) == lastmod then
logger.debug("Time matches, returning 304") >>
IO(
respondWithCacheLastModified(Response[IO](Status.NotModified), zdt)
)
else
logger.debug(lastmod.toString()) >>
logger.debug("Last modified doesn't match, returning 200") >>
IO(
respondWithCacheLastModified(resp, zdt)
)
end if
end response
logger.debug(lastmod.toString()) >>
logger.debug(parseFromHeader(epochInstant, browserLastModifiedAt).toString()) >>
response
}
case _ =>
OptionT.liftF(logger.debug("No headers in query, service it")) >>
service(req).map {
resp =>
respondWithCacheLastModified(
resp,
ZonedDateTime.ofInstant(Instant.ofEpochSecond(lastmod), ZoneId.of("GMT"))
)
}

end match
}

object StaticMiddleware:

def apply(service: HttpRoutes[IO], staticDir: Path)(logger: Scribe[IO]): HttpRoutes[IO] = Kleisli {
(req: Request[IO]) =>
val epochInstant: Instant = Instant.EPOCH
val fullPath = staticDir / req.uri.path.toString.drop(1)

cachedFileResponse(epochInstant, fullPath, req, service)(logger: Scribe[IO])
}
end StaticMiddleware
Loading

0 comments on commit 5c536f2

Please sign in to comment.