early-effect / specular   0.4.0

Apache License 2.0 GitHub

Code-first tests-as-docs site generator for Scala — DocSpecs that assert in CI and SSR-render through ascent.

Scala versions: 3.x
Scala.js versions: 1.x
sbt plugins: 2.x

specular

Scala CI Maven Central License: Apache 2.0

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 until 1.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).


Why specular

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 real ascent.UI values with full-span source capture for the site code panel.
  • One AST, two interpreters — fold the same DocPage into zio-test assertions and a multi-page static site (nav, theme, SSR snapshots).
  • ascent-native — examples SSR through ascent-html and 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.


Installation

specular is published to Maven Central under rocks.earlyeffect. Version is derived from git tags via sbt-dynver (v0.1.00.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)

Authoring a DocSpec

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 assertion

Wire 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.

Docs micro-site vs full site

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).


Modules

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

Build & dogfood

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 scalafmtCheckAll

Requires 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.

Publishing docs (GitHub Pages)

On each v* tag (or Actions → Docs → Run workflow), .github/workflows/docs.yml calls the org reusable workflow early-effect/.githubspecular-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.


Status

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.


License

specular is licensed under the Apache License 2.0.