Skip to content

Commit 5c536f2

Browse files
authored
Merge pull request #23 from Quafadas/frontendRouting
Might serve spa routes
2 parents 5ddda40 + c6cd977 commit 5c536f2

File tree

9 files changed

+515
-216
lines changed

9 files changed

+515
-216
lines changed

.vscode/launch.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"type": "scala",
9+
"request": "attach",
10+
"name": "Attach debugger",
11+
// name of the module that is being debugging
12+
"buildTarget": "project.test",
13+
// Host of the jvm to connect to
14+
"hostName": "localhost",
15+
// Port to connect to
16+
"port": 5005
17+
}
18+
]
19+
}

project/src/live.server.scala

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ case class CliValidationError(message: String) extends NoStackTrace
5757

5858
object LiveServer extends IOApp:
5959
private val logger = scribe.cats[IO]
60+
given filesInstance: Files[IO] = Files.forAsync[IO]
6061

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

119+
val clientRoutingPrefixOpt = Opts
120+
.option[String](
121+
"client-routes-prefix",
122+
"Routes starting with this prefix e.g. /app will return index.html. This enables client side routing via e.g. waypoint"
123+
)
124+
.orNone
125+
118126
val buildToolOpt = Opts
119127
.option[String]("build-tool", "scala-cli or mill")
120128
.validate("Invalid build tool") {
@@ -168,6 +176,7 @@ object LiveServer extends IOApp:
168176
portOpt,
169177
proxyPortTargetOpt,
170178
proxyPathMatchPrefixOpt,
179+
clientRoutingPrefixOpt,
171180
logLevelOpt,
172181
buildToolOpt,
173182
openBrowserAtOpt,
@@ -182,6 +191,7 @@ object LiveServer extends IOApp:
182191
port,
183192
proxyTarget,
184193
pathPrefix,
194+
clientRoutingPrefix,
185195
lvl,
186196
buildTool,
187197
openBrowserAt,
@@ -259,7 +269,7 @@ object LiveServer extends IOApp:
259269
millModuleName
260270
)(logger)
261271

262-
app <- routes(outDirString, refreshTopic, indexOpts, proxyRoutes, fileToHashRef)(logger)
272+
app <- routes(outDirString, refreshTopic, indexOpts, proxyRoutes, fileToHashRef, clientRoutingPrefix)(logger)
263273

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

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

276286
- <- openBrowser(Some(openBrowserAt), port)(logger).toResource
277287
yield server

project/src/ETagMiddleware.scala renamed to project/src/middleware/ETagMiddleware.scala

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import org.http4s.Status
66
import org.http4s.dsl.io.*
77
import org.typelevel.ci.CIStringSyntax
88

9-
import fs2.*
10-
119
import scribe.Scribe
1210

1311
import cats.data.Kleisli
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import org.http4s.Header
2+
import org.http4s.HttpRoutes
3+
import org.http4s.Request
4+
import org.typelevel.ci.CIStringSyntax
5+
6+
import cats.data.Kleisli
7+
import cats.effect.*
8+
import cats.effect.IO
9+
10+
object NoCacheMiddlware:
11+
12+
def apply(service: HttpRoutes[IO]): HttpRoutes[IO] = Kleisli {
13+
(req: Request[IO]) =>
14+
service(req).map {
15+
resp =>
16+
resp.putHeaders(
17+
Header.Raw(ci"Cache-Control", "no-cache")
18+
)
19+
}
20+
}
21+
22+
end NoCacheMiddlware
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import java.time.Instant
2+
import java.time.ZoneId
3+
import java.time.ZonedDateTime
4+
5+
import org.http4s.Header
6+
import org.http4s.HttpRoutes
7+
import org.http4s.Request
8+
import org.http4s.Response
9+
import org.http4s.Status
10+
import org.http4s.dsl.io.*
11+
import org.typelevel.ci.CIStringSyntax
12+
13+
import fs2.*
14+
import fs2.io.file.Path
15+
16+
import scribe.Scribe
17+
18+
import cats.data.Kleisli
19+
import cats.data.OptionT
20+
import cats.effect.*
21+
import cats.effect.IO
22+
import cats.syntax.all.*
23+
24+
def parseFromHeader(epochInstant: Instant, header: String): Long =
25+
java.time.Duration.between(epochInstant, ZonedDateTime.parse(header, formatter)).toSeconds()
26+
end parseFromHeader
27+
28+
object StaticFileMiddleware:
29+
def apply(service: HttpRoutes[IO], file: Path)(logger: Scribe[IO]): HttpRoutes[IO] = Kleisli {
30+
(req: Request[IO]) =>
31+
32+
val epochInstant: Instant = Instant.EPOCH
33+
34+
cachedFileResponse(epochInstant, file, req, service)(logger: Scribe[IO])
35+
}
36+
end StaticFileMiddleware
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import java.time.Instant
2+
import java.time.ZoneId
3+
import java.time.ZonedDateTime
4+
5+
import org.http4s.Header
6+
import org.http4s.HttpRoutes
7+
import org.http4s.Request
8+
import org.http4s.Response
9+
import org.http4s.Status
10+
import org.http4s.dsl.io.*
11+
import org.typelevel.ci.CIStringSyntax
12+
13+
import fs2.*
14+
import fs2.io.file.Path
15+
16+
import scribe.Scribe
17+
18+
import cats.data.Kleisli
19+
import cats.data.OptionT
20+
import cats.effect.*
21+
import cats.effect.IO
22+
import cats.syntax.all.*
23+
24+
inline def respondWithCacheLastModified(resp: Response[IO], lastModZdt: ZonedDateTime) =
25+
resp.putHeaders(
26+
Header.Raw(ci"Cache-Control", "no-cache"),
27+
Header.Raw(ci"ETag", lastModZdt.toInstant.getEpochSecond.toString()),
28+
Header.Raw(
29+
ci"Last-Modified",
30+
formatter.format(lastModZdt)
31+
),
32+
Header.Raw(
33+
ci"Expires",
34+
httpCacheFormat(ZonedDateTime.ofInstant(Instant.now().plusSeconds(10000000), ZoneId.of("GMT")))
35+
)
36+
)
37+
end respondWithCacheLastModified
38+
39+
inline def cachedFileResponse(epochInstant: Instant, fullPath: Path, req: Request[IO], service: HttpRoutes[IO])(
40+
logger: Scribe[IO]
41+
) =
42+
OptionT
43+
.liftF(fileLastModified(fullPath))
44+
.flatMap {
45+
lastmod =>
46+
req.headers.get(ci"If-Modified-Since") match
47+
case Some(header) =>
48+
val browserLastModifiedAt = header.head.value
49+
service(req).semiflatMap {
50+
resp =>
51+
val zdt = ZonedDateTime.ofInstant(Instant.ofEpochSecond(lastmod), ZoneId.of("GMT"))
52+
val response =
53+
if parseFromHeader(epochInstant, browserLastModifiedAt) == lastmod then
54+
logger.debug("Time matches, returning 304") >>
55+
IO(
56+
respondWithCacheLastModified(Response[IO](Status.NotModified), zdt)
57+
)
58+
else
59+
logger.debug(lastmod.toString()) >>
60+
logger.debug("Last modified doesn't match, returning 200") >>
61+
IO(
62+
respondWithCacheLastModified(resp, zdt)
63+
)
64+
end if
65+
end response
66+
logger.debug(lastmod.toString()) >>
67+
logger.debug(parseFromHeader(epochInstant, browserLastModifiedAt).toString()) >>
68+
response
69+
}
70+
case _ =>
71+
OptionT.liftF(logger.debug("No headers in query, service it")) >>
72+
service(req).map {
73+
resp =>
74+
respondWithCacheLastModified(
75+
resp,
76+
ZonedDateTime.ofInstant(Instant.ofEpochSecond(lastmod), ZoneId.of("GMT"))
77+
)
78+
}
79+
80+
end match
81+
}
82+
83+
object StaticMiddleware:
84+
85+
def apply(service: HttpRoutes[IO], staticDir: Path)(logger: Scribe[IO]): HttpRoutes[IO] = Kleisli {
86+
(req: Request[IO]) =>
87+
val epochInstant: Instant = Instant.EPOCH
88+
val fullPath = staticDir / req.uri.path.toString.drop(1)
89+
90+
cachedFileResponse(epochInstant, fullPath, req, service)(logger: Scribe[IO])
91+
}
92+
end StaticMiddleware

0 commit comments

Comments
 (0)