Tests-as-docs for Scala 3. A documentation page is a real program — a DocSpec that
compiles with your build, asserts under zio-test, and SSR-renders through
ascent into a static site. Examples cannot drift from
behavior: a red example fails CI.
Status: early / pre-1.0. Published under early-semver (
versionScheme := "early-semver") — the API can change between minor versions until1.0.
import specular.*
import specular.ziotest.DocSpecSuite
import ascent.*, ascent.dsl.*
import zio.test.*
object GettingStarted extends DocSpecSuite:
def doc = page("Getting started")(
md"""Ascent renders **directly to the DOM**. No virtual DOM, no diffing.""",
section("A pure value")(
example {
E.ul(E.li("a"), E.li("b"))
}.assert(_ => assertTrue(true)),
),
section("A live counter")(
exampleIO {
for count <- sq(0)
yield E.div(
E.button(Events.onClick(_ => count.update(_ + 1)), "+"),
E.span(count.map(_.toString)),
)
}.interactive.assert(_ => assertTrue(true)),
),
)Put DocSpecs under src/test. The same page is a zio-test suite (DocSpecSuite) and feeds
the static site via DocsSite. Interactive examples mount in the browser via a Scala.js
client bundle (optional).
Most doc tools are markdown-first: you write prose, embed code fences, and hope a separate pipeline keeps them honest. specular flips that:
- Code-first — the page is Scala. Prose is embedded with
md"""…"""; examples are realascent.UIvalues with full-span source capture for the site code panel. - One AST, two interpreters — fold the same
DocPageinto zio-test assertions and a multi-page static site (nav, theme, SSR snapshots). - ascent-native — examples SSR through
ascent-htmland can remount live in the reader’s browser. Docs dogfood the UI library they describe.
Use it for a library docs micro-site, or for a full project / org hub (landing page,
project catalog, themes). Each published micro-site emits a metadata.json so an org hub
(like earlyeffect.rocks) can compose library cards from live
version metadata.
The dogfood site expands this story for adopters: Why Specular, Getting started, Concepts, and Library authors.
specular is published to Maven Central under rocks.earlyeffect. Version is derived from git
tags via sbt-dynver (v0.1.0 → 0.1.0).
libraryDependencies ++= Seq(
"rocks.earlyeffect" %% "specular-core" % "<version>" % Test,
"rocks.earlyeffect" %% "specular-zio-test" % "<version>" % Test,
"rocks.earlyeffect" %% "specular-site" % "<version>" % Test,
)
// sbt plugin: injects product meta and runs specularSite from the Test classpath
addSbtPlugin("rocks.earlyeffect" % "sbt-specular" % "<version>")specular-core is also available for Scala.js (%%%) when your docs client needs the AST.
enablePlugins(SpecularPlugin)
specularBuildMain := "com.example.docs.BuildSite"
specularMetaProject := Some(LocalProject("root")) // published module identity
specularArtifactKind := "library" // or "plugin"// docs/src/test/scala/.../BuildSite.scala
object BuildSite extends specular.site.DocsSite:
def pages = Vector(GettingStarted.doc, Concepts.doc)def page(title: String)(nodes: DocNode*): DocPage
def section(title: String)(nodes: DocNode*): Section
def md"""…""": Prose // markdown → ascent UI
def example { ui }: Example[Any] // static UI + source capture
def exampleIO { urio }: Example[Any] // effectful UI (e.g. sq(0))
example.interactive // also mount client-side
example.assert(ui => assertTrue(…)) // zio-test assertionWire the page with DocSpecSuite (tests) and DocsSite (site map):
object GettingStarted extends DocSpecSuite:
def doc = page("Getting started")(…)
object BuildSite extends DocsSite:
def pages = Vector(GettingStarted.doc, Concepts.doc)sbt test discovers DocSpecSuites; sbt docs/specularSite forks BuildSite on the Test
classpath with -Dspecular.meta.* from specularMetaProject.
| Mode | Configure | Output |
|---|---|---|
| Docs-only | SiteModel(title, pages) (+ optional theme / meta) |
Sidebar docs + index + metadata.json |
| Full site | brand, home (hero, ProjectCatalog, …) |
Landing page + optional deep links to micro-sites |
Themes: Theme.default or Theme.fromTokens(...). Early Effect projects should depend on
early-effect-docs-theme for hub-matched tokens and the logo resource:
libraryDependencies += "rocks.earlyeffect" %% "early-effect-docs-theme" % "<version>"
// SiteModel(..., logo = Some(EarlyEffectTheme.logoHref))
// .provide(..., EarlyEffectTheme.live, ...)
// EarlyEffectTheme.writeLogo(outDir)Set SiteModel.logo for a small header mark beside the project name; the brand link goes to
index.html.
Every site build writes metadata.json next to index.html (name, org, version, pages, …)
so hubs can fetch published manifests instead of hardcoding library cards.
ProjectCatalog.fromMetadataUrls only accepts http(s) URLs (trusted allowlist — not an open proxy).
| Module | Artifact | Role |
|---|---|---|
core |
specular-core |
DocPage / DocNode AST, example / md / section (depends on zio-test for .assert) |
zio-test |
specular-zio-test |
Run DocSpecs as zio-test suites |
site |
specular-site |
Markdown → UI, SSR, themes, templates, metadata.json |
early-effect-docs-theme |
early-effect-docs-theme |
EE hub tokens + logo (optional brand pack; not required for Specular) |
sbt-specular |
sbt-specular |
specularSite task; passes -Dspecular.meta.* from sbt keys |
docs |
(unpublished) | Dogfood site for specular itself |
sbt test # unit + DocSpec tests
sbt docs/specularSite # link JS client + write target/site (incl. metadata.json)
sbt docs/run # preview server via sbt-reload (`docs/runReload`)
./scripts/install-git-hooks # once per clone: pre-commit runs scalafmtCheckAllRequires a JDK that can run Scala 3.8 / sbt 2 (CI uses Temurin 25). Interactive examples need
the docs JS link (docsJS/fastLinkJS), which docs/specularSite runs for you.
On each v* tag (or Actions → Docs → Run workflow), .github/workflows/docs.yml calls the org reusable
workflow early-effect/.github → specular-docs.yml.
That builds docs/specularSite and deploys to GitHub Pages at
https://early-effect.github.io/specular/.
CI sets SPECULAR_BASE_PATH=/specular and SPECULAR_DOCS_URL=https://early-effect.github.io/specular/
so nav links and metadata.json match the project-site URL. Local preview keeps basePath = ".".
Enable Settings → Pages → Source: GitHub Actions on this repo before the first deploy. Use the manual run when you need to regenerate docs without cutting a new tag.
The org hub (earlyeffect.rocks) composes library cards from published
metadata.json URLs. After docs are live, add/refresh the catalog in
early-effect.github.io and run its
Hub site workflow.
specular is early and evolving. The core loop (DocSpec → tests + multi-page site + interactive
examples) works and is dogfooded here; themes, landing templates, and metadata.json hub
composition are in place and still settling. Expect breaking changes between minor versions
until 1.0.
specular is licensed under the Apache License 2.0.