Skip to content

Repository files navigation

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, // includes mermaid Prose fences
)
// docs JS client (when you remount interactive diagrams):
// libraryDependencies += "rocks.earlyeffect" %%% "specular-mermoid" % "<version>"

// 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), diagramInteractive)
example.interactive                                           // also mount client-side
example.assert(ui => assertTrue(…))                           // zio-test assertion

Mermaid diagrams use specular-mermoid (via specular-site): fenced mermaid in Prose and Mermoid.diagram render hybrid HTML+SVG at build time; exampleIO { Mermoid.diagramInteractive(…) }.interactive adds selection, tooltips, and viewport reflow in the browser.

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(...). DocsSite.standardLayers is the stock stack; DocsSite.themedStack is the same stack with Theme left as an environment hole, so any theme layer composes in:

override def layers = Theme.fromTokens(myTokens) >>> DocsSite.themedStack

Early Effect projects should depend on early-effect-docs-theme for hub-matched tokens and logo PNGs — it pre-composes the stack, so branding is three one-liners:

libraryDependencies += "rocks.earlyeffect" %% "early-effect-docs-theme" % "<version>"

override def site   = EarlyEffectTheme.brand(super.site)   // header logo + hub link
override def layers = EarlyEffectTheme.layers              // EE tokens >>> DocsSite.themedStack
override def afterBuild(out: Path, result: SiteOutput) = EarlyEffectTheme.writeLogo(out)

brand only fills fields the caller left unset, so an explicit logo / logoLink still wins. Use EarlyEffectTheme.heroImageHref on landing heroes. The brand title links 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 / ProjectCatalog.live only accept http(s) URLs (trusted allowlist — not an open proxy). Live hubs ship an Ascent client (LiveCatalog.bootstrap) so refresh picks up new versions; rebuild the hub when the allowlist changes.


Modules

Module Artifact Role
core specular-core DocPage / DocNode AST, example / md / section, shared ProjectMeta / catalog cards (+ JS LiveCatalog)
zio-test specular-zio-test Run DocSpecs as zio-test suites
site specular-site Markdown → UI (incl. fenced mermaid), SSR, themes, templates, metadata.json, JVM meta fetch
mermoid specular-mermoid mermoid via mermoid-ascent: Mermoid.diagram (hybrid), diagramInteractive (selection/reflow), svgDiagram (inert); pulled in by site on JVM; %%% for Scala.js remount
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 docsDev              # watch docs: rebuild site + restart preview (http://127.0.0.1:8765)
./scripts/install-git-hooks   # once per clone: pre-commit runs scalafmtCheckAll

docsDev is ~docs/Test/runReload (sbt-reload): each source change runs docs/specularSite, then restarts DocsServe. Press Enter to leave watch mode.

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.

About

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

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages