Getting started
This page is itself a DocSpec: the same source runs as a zio-test suite and builds the
static site you are reading. Follow the steps below to wire Specular into a Scala 3 / sbt
library.
1. Add the artifacts
Publish line is Maven Central under rocks.earlyeffect (see the README badge for the
current version). Docs live on the Test classpath by convention:
// project/plugins.sbt
addSbtPlugin("rocks.earlyeffect" % "sbt-specular" % "<version>")
// build.sbt (docs project)
enablePlugins(SpecularPlugin)
libraryDependencies ++= Seq(
"rocks.earlyeffect" %% "specular-core" % "<version>" % Test,
"rocks.earlyeffect" %% "specular-zio-test" % "<version>" % Test,
"rocks.earlyeffect" %% "specular-site" % "<version>" % Test,
)
specularBuildMain := "com.example.docs.BuildSite"
specularMetaProject := Some(LocalProject("root")) // product identity, not the docs module
specularArtifactKind := "library" // or "plugin"
specular-core is also available for Scala.js (%%%) when you ship interactive examples.
2. Author a DocSpec
A page is page / section / md / example. Prose is markdown; UI examples are ascent
UI values whose full source span is captured for the site panel. Plain Scala and ZIO use
exampleValue / exampleZIO (same ValueExample node: source + printed result).
E.ul(E.li("a"), E.li("b"), E.li("c"))- a
- b
- c
Effectful UIs (state with sq, IO) use exampleIO. Mark them .interactive so the
Scala.js client remounts them in the browser after SSR.
for count <- sq(0)
yield E.div(
E.button(Events.onClick(_ => count.update(_ + 1)), "+"),
E.span(" count: ", count.map(_.toString)),
)For non-UI libraries, assert a value or effect outcome directly:
"specular".length83. Run examples as tests
Only examples with .assert become zio-test cases. Prefer DocSpecSuite so the page is
the suite (no separate *Spec.scala):
import specular.*
import specular.ziotest.DocSpecSuite
import zio.test.*
object GettingStarted extends DocSpecSuite:
def doc = page("Getting started")(
exampleValue(1 + 1).assert(n => assertTrue(n == 2)),
)
Put that under docs/src/test/scala. sbt test discovers it like any other zio-test suite.
Unasserted snapshots still render on the site; they just do not gate CI.
If you also need a Scala.js client for .interactive examples, keep shared pages as
DocSpec and add thin DocSpecSuite wrappers on the JVM only (see Library authors).
E.ul(
E.li(E.code(".assert"), " → suite test"),
E.li(E.code(".interactive"), " → client mount"),
E.li("plain ", E.code("example"), " → SSR only"),
).assert→ suite test.interactive→ client mount- plain
example→ SSR only
4. Build the site
Extend DocsSite with an ordered page list (your site map / nav order):
import specular.site.*
object BuildSite extends DocsSite:
def pages = Vector(GettingStarted.doc, Concepts.doc)
// optional: override site / layers / afterBuild
Also under src/test. sbt docs/specularSite compiles Test, forks that main with product
meta from specularMetaProject, and writes HTML plus metadata.json.
Local loop:
sbt test
sbt docs/specularSite
sbt docs/specularServe # preview
E.div(A.className("demo"), E.p("Hello from Specular"))Hello from Specular
5. Publish on GitHub Pages
Any static host works; GitHub Pages is the common path. Enable Settings → Pages →
Source: GitHub Actions, then deploy specularSite output on v* tags (and optional
workflow_dispatch).
One reusable-workflow example (copy and point sbt-project at your docs module):
# .github/workflows/docs.yml
name: Docs
on:
push:
tags: ['v*']
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
jobs:
deploy:
uses: early-effect/.github/.github/workflows/specular-docs.yml@main
with:
sbt-project: docs
Set SPECULAR_BASE_PATH and SPECULAR_DOCS_URL in CI so nav and metadata.json match
the published project-site URL (for example /my-lib under *.github.io).
Next: Concepts for the AST and interpreters, or Library authors for a full cookbook.