Skip to content

Instantly share code, notes, and snippets.

@corlaez
Last active August 31, 2022 13:32
Show Gist options
  • Save corlaez/f5da5edc59542dbf2c6d26547256bc79 to your computer and use it in GitHub Desktop.
Save corlaez/f5da5edc59542dbf2c6d26547256bc79 to your computer and use it in GitHub Desktop.
Simple browser auto-reloader in Kotlin

In this example there is a bunch of java/kotlin nonsense, but I wanted to paint as much of the picture as possible. However, the idea should be pretty universal.

Run Modes

arg Outcome Recommended Usage
dev generate and serve with hot reload Dev server
prod generate and serve without hot reload Check website before a deploy
regenerate http request to regenerate website Run manually or use it in File Watchers

dev serve wil:

  • Add additional endpoints in the server to subscribe browsers and notify that a regeneration has finished
  • Add a script to the html pages that subscribes and reloads the pages when a message is receibed

While not a hot module replacement, for simple sites this is all that you need, a simple web refresh on code change. Oh right, link the regenerate process to your file watcher of choice. I use IntelliJ's file watcher plugin.

IntelliJ's File Watcher Config

While pages served with the included server in dev mode will hot reload if the generation is executed we still have to trigger that manually. To avoid this, File Watcher plugin can be used to trigger generation when saving files.

This is the recommended configuration:

  • File Type: Any
  • Scope: Open Files
  • Program: Point for gradlew in the root of the project
  • Arguments: run --args="regenerate"
  • Auto-save edited files to trigger the watcher: Checked
  • Trigger on external changes: Checked (IntelliJ CSS color picker triggers an external change)
  • Trigger regardless of syntax errors: Unchecked
  • Show console: Always

(I might have also changed a genereal IntelliJ setting to save to disk without me pressing control+s but I am not sure about that)

Screenshots

File watcher running in the background

image

A convenient log on the server once browsers have been sent a message to reload

image

<html>
<head>
<!-- ... -->
<!-- I inject this script based on a dev arg in EnvContext -->
<script async="async" defer="defer">
const ws = new WebSocket("ws://localhost:8080/dev/subscribe")
ws.onopen=()=>console.log("open")
ws.onmessage=()=>console.log("reload")||location.reload()
ws.onerror=(ev)=>console.log(ev)
ws.onclose=()=>{console.log("close")}</script>
</head>
<body>
<!-- ... -->
</body>
</html>
import org.eclipse.jetty.http.HttpStatus
import java.time.LocalDateTime
fun main(args: Array<String>) {
val arg = Args.valueOf(args[0])
val port = System.getenv("PORT") ?: "8080"
if (arg.isRegenerate()) {
val serverArg = getRequestArg(port)
with(EnvContext(serverArg, port, webPlugins)) {
generate()// Here I generate my static website
devGetRequestReload()// Notify server that files have been updated
}
} else {
with(EnvContext(arg, port, webPlugins)) {
generate()// Here I generate my static website
serve()
}
}
}
private fun getRequestArg(port: String): Args {
val (argStatus, argResponse) = httpGet("http://localhost:$port/dev/arg")
return if (argStatus != HttpStatus.OK_200) {
logger.warn("server failed to provide arg (generating as prd). Code: $argStatus")
Args.prd
} else {
Args.valueOf(argResponse)
}
}
context(EnvContext)
private fun devGetRequestReload() {
if (arg.isDev()) {
val (reloadStatus, reloadResponse) = httpGet("http://localhost:$port/dev/reload")
if (reloadStatus != HttpStatus.OK_200) error("dev server failed to reload clients. Code: $reloadStatus")
logger.info("$reloadResponse ${LocalDateTime.now()}")
}
}
fun httpGet(urlStr: String): Pair<Int, String> {
val url = URL(urlStr)
val con = url.openConnection() as HttpURLConnection
con.requestMethod = "GET"
return runCatching {
con.responseCode to con.inputStream.bufferedReader().readText()
}.getOrDefault(HttpStatus.INTERNAL_SERVER_ERROR_500 to "Couldn't connect")
}
data class EnvContext(val arg: Args, val port: String)
enum class Args {
dev, prd, regenerate;
fun isDev() = this == dev
fun isPrd() = this == prd || this == prdWithoutServer
fun isRegenerate() = this == regenerate
}
import io.javalin.Javalin
import io.javalin.websocket.WsContext
import java.time.LocalTime
context(EnvContext)
fun serve() {
val app = Javalin.create { config ->
// javalin static file rendering config
}.start(port.toInt())
// Share the arg used to start the server. Allows regeneration to use the same arg that the server used
app.get("/dev/arg") { ctx ->
ctx.result(arg.toString())
}
devConfig(app)
logger.info("Listening on http://localhost:$port/")
if (arg.isPrd()) logger.info("This server does not support hot reloading. Please reload the browser manually")
}
context(EnvContext)
private fun devConfig(app: Javalin) {
if (arg.isDev()) {
val wsContexts = mutableListOf<WsContext>()
app.ws("/dev/subscribe") { ws ->
ws.onConnect { wsContexts += it }
ws.onClose { ctx -> wsContexts.remove(ctx) }
}
app.get("/dev/reload") { ctx ->
wsContexts.forEach { it.send("reload") }
val message = "${wsContexts.size} clients Reloaded! ${LocalTime.now()}"
logger.info(message)
ctx.result(message)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment