Preview
ascent-preview is a published JVM library: path-jailed static files plus an SSE endpoint that
fires when assets/dev-stamp contents change. The tab reloads. The Preview process does not.
That is the whole contract. HTML and JS may churn on disk; rewrite the stamp to poke the browser.
Never watch a task that kills the server (~runReload, ~preview/run).
The command to remember is sbt ~<module>/ascentPreview. ~ is sbt's file watch on a task
that keeps Preview up. One-shot (start once, no watch): sbt <module>/ascentPreview.
Install
Library (JVM):
libraryDependencies += "rocks.earlyeffect" %% "ascent-preview" % "<version>"
Plugin (the command):
addSbtPlugin("rocks.earlyeffect" % "sbt-ascent-preview" % "<version>")
Then enablePlugins(AscentPreviewPlugin) on the module you type. Specular docs modules will get
that by default once sbt-specular requires this plugin; until then, enable it on docs and set
ascentPreviewRoot to the site directory and ascentPreviewRebuild to specularSiteDev.
Scala.js app
lazy val todo = (project in file("example/todo"))
.enablePlugins(ScalaJSPlugin, AscentPreviewPlugin)
.settings(
// JS projects have no PreviewMain on Compile. Point at a JVM ascent-preview classpath:
ascentPreviewClasspath := (LocalProject("preview") / Compile / fullClasspath).value,
)
sbt ~todoJS/ascentPreview
# open http://localhost:8765
Default rebuild is ascentPreviewStage: spliceFast (if sbt-splice is on the project), copy
index.html, write assets/dev-stamp. Override ascentPreviewBundle for plain fastLinkJS.
Specular docs
Docs are the same command. The rebuild task writes target/site (including the stamp) instead of
an example target/preview.
.enablePlugins(SpecularPlugin, AscentPreviewPlugin)
.settings(
ascentPreviewRoot := specularSiteDirectory.value,
ascentPreviewRebuild := specularSiteDev.value,
ascentPreviewAutoOpen := true, // open the tab once Preview binds
ascentPreviewPort := AscentPreviewPort("auto"), // first free port >= 8700
)
sbt ~docs/ascentPreview
# open the localhost URL sbt prints (port is auto)
specularServe stays a blocking one-shot of an already-built tree. The edit loop is only
ascentPreview.
Compose into zio-http
When a JVM app already serves the staged tree (datastar / hybrid examples), do not start a second
PreviewMain. Set ascentPreviewAutoServe := false on the JS module and keep the API server up:
Preview.routes(PreviewConfig(root = previewRoot, port = 8080)) ++ apiRoutes
sbt ~datastarExampleJS/ascentPreview # stage + stamp only
sbt datastarExampleServer/run # Preview.routes + API on :8080
The server process stays up. Only the files under target/preview change.
Tab client
From ascent-js (localhost only; inert on GitHub Pages):
DevReload.install() // EventSource /__ascent/reload → location.reload()
Specular's SpecularClient.mountAll already calls this. Do not also poll assets/dev-stamp.
Without Scala.js, a tiny snippet in index.html:
<script>
if (["localhost","127.0.0.1","[::1]"].includes(location.hostname)) {
const es = new EventSource("/__ascent/reload");
const reload = () => location.reload();
es.addEventListener("reload", reload);
es.addEventListener("message", reload);
es.addEventListener("error", () => es.close());
}
</script>
Config and jail
PreviewConfig(root, port = 8765, stamp = assets/dev-stamp, reloadPath = __ascent/reload, cors = false, openBrowser = false).
CORS is off by default so same-origin example servers stay strict.
Paths are jailed: .. is rejected, and the resolved file must be a canonical descendant of root.
CLI: PreviewMain <port> <siteRoot> [--open] (default 8765 and target/site). --open is
what ascentPreviewAutoOpen := true passes so the tab opens once the socket is bound, not on every
~ rebuild.
Module bind: ascentPreviewPort := AscentPreviewPort(8701) or
ascentPreviewPort := AscentPreviewPort("auto") (first free port >= 8700).
Neotype rejects "noo auto" and privileged ports like 80 at compile time.
Anti-pattern
sbt ~docs/Test/runReload # restarts the Preview JVM on every compile
sbt ~preview/run # same: the process is the watch target
Watch the rebuild (ascentPreview / ascentPreviewStage / specularSite), not the server.