Skip to content

Commit

Permalink
Merge pull request #35 from Quafadas/preloadInternal2
Browse files Browse the repository at this point in the history
Preload "internal-xxx" modules to accelerate browser loading
  • Loading branch information
Quafadas authored Jul 16, 2024
2 parents 7deb64d + ef315d0 commit b4907cb
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 89 deletions.
11 changes: 9 additions & 2 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ compile:
mill project.compile

test:
mill project.test
mill project.test.testOnly SafariSuite && mill project.test.testOnly RoutesSuite && mill project.test.testOnly UtilityFcs

checkOpts:
mill project.run --help
Expand All @@ -33,7 +33,14 @@ jvmLinker:
mill project.run --build-tool scala-cli --project-dir /Users/simon/Code/helloScalaJs --out-dir /Users/simon/Code/helloScalaJs/out --extra-build-args --js-cli-on-jvm --port 3007

serveMill:
mill project.run --build-tool mill --project-dir /Users/simon/Code/mill-full-stack/mill-full-stack --styles-dir /Users/simon/Code/mill-full-stack/mill-full-stack/frontend/ui/assets --out-dir /Users/simon/Code/mill-full-stack/mill-full-stack/out/frontend/fastLinkJS.dest --log-level info --proxy-target-port 8080 --proxy-prefix-path /api --port 3007 --mill-module-name frontend
mill -j 0 project.run --build-tool mill --project-dir /Users/simon/Code/mill-full-stack/mill-full-stack \
--path-to-index-html /Users/simon/Code/mill-full-stack/mill-full-stack/frontend/ui \
--out-dir /Users/simon/Code/mill-full-stack/mill-full-stack/out/frontend/fastLinkJS.dest \
--log-level info \
--port 3007 \
--mill-module-name frontend \
--proxy-prefix-path /api \
--proxy-target-port 8080

setupPlaywright:
cs launch com.microsoft.playwright:playwright:1.41.1 -M "com.microsoft.playwright.CLI" -- install --with-deps
Expand Down
71 changes: 43 additions & 28 deletions project/src/htmlGen.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import scalatags.Text.all.*

import fs2.io.file.Path
import cats.effect.kernel.Ref
import cats.effect.IO

def lessStyle(withStyles: Boolean): Seq[Modifier] =
if withStyles then
Expand Down Expand Up @@ -49,22 +51,17 @@ def injectRefreshScript(template: String) =

end injectRefreshScript

def injectModulePreloads(modules: Seq[(Path, String)], template: String) =
val modulesStrings =
for
m <- modules
if m._1.toString.endsWith(".js")
yield link(rel := "modulepreload", href := s"${m._1}?hash=${m._2}").render
def injectModulePreloads(ref: Ref[IO, Map[String, String]], template: String) =
val preloads = makeInternalPreloads(ref)
preloads.map: modules =>
val modulesStringsInject = modules.mkString("\n", "\n", "\n")
val headCloseTag = "</head>"
val insertionPoint = template.indexOf(headCloseTag)

val modulesStringsInject = modulesStrings.mkString("\n", "\n", "\n")
val headCloseTag = "</head>"
val insertionPoint = template.indexOf(headCloseTag)

val newHtmlContent = template.substring(0, insertionPoint) +
modulesStringsInject +
template.substring(insertionPoint)

newHtmlContent
val newHtmlContent = template.substring(0, insertionPoint) +
modulesStringsInject +
template.substring(insertionPoint)
newHtmlContent

end injectModulePreloads

Expand Down Expand Up @@ -93,17 +90,35 @@ def makeHeader(modules: Seq[(Path, String)], withStyles: Boolean) =
)
end makeHeader

def vanillaTemplate(withStyles: Boolean) = html(
head(
meta(
httpEquiv := "Cache-control",
content := "no-cache, no-store, must-revalidate"
def makeInternalPreloads(ref: Ref[IO, Map[String, String]]) =
val keys = ref.get.map(_.toSeq)
keys.map {
modules =>
for
m <- modules
if m._1.toString.endsWith(".js") && m._1.toString.startsWith("internal")
yield link(rel := "modulepreload", href := s"${m._1}?h=${m._2}")
end for
}

end makeInternalPreloads

def vanillaTemplate(withStyles: Boolean, ref: Ref[IO, Map[String, String]]) =
val preloads = makeInternalPreloads(ref)
preloads.map: modules =>
html(
head(
meta(
httpEquiv := "Cache-control",
content := "no-cache, no-store, must-revalidate",
modules
)
),
body(
lessStyle(withStyles),
script(src := "/main.js", `type` := "module"),
div(id := "app"),
refreshScript
)
)
),
body(
lessStyle(withStyles),
script(src := "/main.js", `type` := "module"),
div(id := "app"),
refreshScript
)
)
end vanillaTemplate
19 changes: 17 additions & 2 deletions project/src/middleware/ETagMiddleware.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,25 @@ object ETagMiddleware:
map.get(req.uri.path.toString.drop(1)) match
case Some(hash) =>
logger.debug(req.uri.toString) >>
IO(resp.putHeaders(Header.Raw(ci"ETag", hash)))
IO(
resp.putHeaders(
Header.Raw(ci"ETag", hash),
Header.Raw(ci"Cache-control", "Must-Revalidate"),
Header.Raw(ci"Cache-control", "No-cache"),
Header.Raw(ci"Cache-control", "max-age=0"),
Header.Raw(ci"Cache-control", "public")
)
)
case None =>
logger.debug(req.uri.toString) >>
IO(resp)
IO(
resp.putHeaders(
Header.Raw(ci"Cache-control", "Must-Revalidate"),
Header.Raw(ci"Cache-control", "No-cache"),
Header.Raw(ci"Cache-control", "max-age=0"),
Header.Raw(ci"Cache-control", "public")
)
)
end match
}
end respondWithEtag
Expand Down
114 changes: 77 additions & 37 deletions project/src/routes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import org.http4s.server.middleware.Logger
import org.http4s.server.staticcontent.*
import org.http4s.server.staticcontent.FileService
import org.typelevel.ci.CIStringSyntax
import org.http4s.EntityBody

import fs2.*
import fs2.concurrent.Topic
Expand All @@ -36,6 +37,7 @@ import cats.effect.kernel.Resource
import cats.syntax.all.*

import _root_.io.circe.syntax.EncoderOps
import org.http4s.Http

def routes[F[_]: Files: MonadThrow](
stringPath: String,
Expand Down Expand Up @@ -70,8 +72,8 @@ def routes[F[_]: Files: MonadThrow](
ref
)(logger)

val hashFalse = vanillaTemplate(false).render.hashCode.toString
val hashTrue = vanillaTemplate(true).render.hashCode.toString
// val hashFalse = vanillaTemplate(false).render.hashCode.toString
// val hashTrue = vanillaTemplate(true).render.hashCode.toString
val zdt = ZonedDateTime.now()

def userBrowserCacheHeaders(resp: Response[IO], lastModZdt: ZonedDateTime, injectStyles: Boolean) =
Expand All @@ -97,73 +99,95 @@ def routes[F[_]: Files: MonadThrow](
object StaticHtmlMiddleware:
def apply(service: HttpRoutes[IO], injectStyles: Boolean)(logger: Scribe[IO]): HttpRoutes[IO] = Kleisli {
(req: Request[IO]) =>
req.headers.get(ci"If-None-Match").map(_.toList) match
case Some(h :: Nil) if h.value == hashFalse => OptionT.liftF(IO(Response[IO](Status.NotModified)))
case Some(h :: Nil) if h.value == hashTrue => OptionT.liftF(IO(Response[IO](Status.NotModified)))
case _ => service(req).semiflatMap(userBrowserCacheHeaders(_, zdt, injectStyles))
end match

service(req).semiflatMap(userBrowserCacheHeaders(_, zdt, injectStyles))
}

end StaticHtmlMiddleware

def generatedIndexHtml(injectStyles: Boolean) =
def generatedIndexHtml(injectStyles: Boolean, modules: Ref[IO, Map[String, String]]) =
StaticHtmlMiddleware(
HttpRoutes.of[IO] {
case req @ GET -> Root =>
logger.trace("Generated index.html") >>
IO(
vanillaTemplate(injectStyles, modules).map: html =>
Response[IO]()
.withEntity(vanillaTemplate(injectStyles))
.withEntity(html)
.withHeaders(
Header.Raw(ci"Cache-Control", "no-cache"),
Header.Raw(
ci"ETag",
injectStyles match
case true => hashTrue
case false => hashFalse
html.hashCode.toString
),
Header.Raw(ci"Last-Modified", formatter.format(zdt)),
Header.Raw(
ci"Expires",
httpCacheFormat(ZonedDateTime.ofInstant(Instant.now().plusSeconds(10000000), ZoneId.of("GMT")))
)
)
)

},
injectStyles
)(logger).combineK(
StaticHtmlMiddleware(
HttpRoutes.of[IO] {
case GET -> Root / "index.html" =>
IO {
Response[IO]().withEntity(vanillaTemplate(injectStyles))
}
vanillaTemplate(injectStyles, modules).map: html =>
Response[IO]().withEntity(html)

},
injectStyles
)(logger)
)

// val formatter = DateTimeFormatter.RFC_1123_DATE_TIME
val staticAssetRoutes: HttpRoutes[IO] = indexOpts match
case None => generatedIndexHtml(injectStyles = false)
def staticAssetRoutes(modules: Ref[IO, Map[String, String]]): HttpRoutes[IO] = indexOpts match
case None => generatedIndexHtml(injectStyles = false, modules)

case Some(IndexHtmlConfig.IndexHtmlPath(path)) =>
StaticMiddleware(
Router(
"" -> fileService[IO](FileService.Config(path.toString()))
),
fs2.io.file.Path(path.toString())
)(logger)
// StaticMiddleware(
// Router(
// "" ->
HttpRoutes
.of[IO] {
case req @ GET -> Root =>
StaticFile
.fromPath[IO](path / "index.html")
.getOrElseF(NotFound())
.flatMap {
f =>
f.body
.through(text.utf8.decode)
.compile
.string
.flatMap {
body =>
for str <- injectModulePreloads(modules, body)
yield
val bytes = str.getBytes()
f.withEntity(bytes)
Response[IO]().withEntity(bytes).putHeaders("Content-Type" -> "text/html")

}
}

}
.combineK(
StaticMiddleware(
Router(
"" -> fileService[IO](FileService.Config(path.toString()))
),
fs2.io.file.Path(path.toString())
)(logger)
)

case Some(IndexHtmlConfig.StylesOnly(stylesPath)) =>
NoCacheMiddlware(
Router(
"" -> fileService[IO](FileService.Config(stylesPath.toString()))
)
)(logger).combineK(generatedIndexHtml(injectStyles = true))
)(logger).combineK(generatedIndexHtml(injectStyles = true, modules))

val clientSpaRoutes: HttpRoutes[IO] =
def clientSpaRoutes(modules: Ref[IO, Map[String, String]]): HttpRoutes[IO] =
clientRoutingPrefix match
case None => HttpRoutes.empty[IO]
case Some(spaRoute) =>
Expand All @@ -173,9 +197,9 @@ def routes[F[_]: Files: MonadThrow](
StaticHtmlMiddleware(
HttpRoutes.of[IO] {
case req @ GET -> root /: path =>
IO(
Response[IO]().withEntity(vanillaTemplate(false))
)
vanillaTemplate(false, modules).map: html =>
Response[IO]().withEntity(html)

},
false
)(logger)
Expand All @@ -184,9 +208,8 @@ def routes[F[_]: Files: MonadThrow](
StaticHtmlMiddleware(
HttpRoutes.of[IO] {
case GET -> root /: spaRoute /: path =>
IO(
Response[IO]().withEntity(vanillaTemplate(true))
)
vanillaTemplate(true, modules).map: html =>
Response[IO]().withEntity(html)
},
true
)(logger)
Expand All @@ -195,7 +218,24 @@ def routes[F[_]: Files: MonadThrow](
StaticFileMiddleware(
HttpRoutes.of[IO] {
case req @ GET -> spaRoute /: path =>
StaticFile.fromPath(dir / "index.html", Some(req)).getOrElseF(NotFound())
StaticFile
.fromPath(dir / "index.html", Some(req))
.getOrElseF(NotFound())
.flatMap {
f =>
f.body
.through(text.utf8.decode)
.compile
.string
.flatMap: body =>
for str <- injectModulePreloads(modules, body)
yield
val bytes = str.getBytes()
f.withEntity(bytes)
f

}

},
dir / "index.html"
)(logger)
Expand All @@ -215,8 +255,8 @@ def routes[F[_]: Files: MonadThrow](
refreshRoutes
.combineK(linkedAppWithCaching)
.combineK(proxyRoutes)
.combineK(clientSpaRoutes)
.combineK(staticAssetRoutes)
.combineK(clientSpaRoutes(ref))
.combineK(staticAssetRoutes(ref))
)

clientRoutingPrefix.fold(IO.unit)(s => logger.trace(s"client spa at : $s")).toResource >>
Expand Down
31 changes: 16 additions & 15 deletions project/test/src/UtilityFcts.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
class UtilityFcs extends munit.FunSuite:
import munit.CatsEffectSuite
import cats.effect.kernel.Ref
import cats.effect.IO

class UtilityFcs extends CatsEffectSuite:

test("That we actually inject the preloads ") {

Expand All @@ -14,20 +18,17 @@ class UtilityFcs extends munit.FunSuite:

}

test(" That we can inject preloads into a template") {
val html = injectModulePreloads(
modules = Seq(
(fs2.io.file.Path("main.js"), "hash")
),
template = "<html><head></head></html>"
)
assert(html.contains("hash"))
assertEquals(
html,
"""<html><head>
<link rel="modulepreload" href="main.js?hash=hash" />
</head></html>"""
)
ResourceFunFixture {
for
ref <- Ref.of[IO, Map[String, String]](Map.empty).toResource
_ <- ref.update(_.updated("internal.js", "hash")).toResource
yield ref
}.test("That we can make internal preloads") {
ref =>
val html = injectModulePreloads(ref, "<html><head></head><body></body></html>")
html.map: html =>
assert(html.contains("modulepreload"))
assert(html.contains("internal.js"))
}

test(" That we can inject a refresh script") {
Expand Down
Loading

0 comments on commit b4907cb

Please sign in to comment.