Skip to content

v0.12.0: Make interactive examples framework-agnostic via a keyed DOM mount (#56)

Latest

Choose a tag to compare

@russwyte russwyte released this 08 Aug 16:11
· 1 commit to main since this release
0706ff7
* Make interactive examples framework-agnostic via a keyed DOM mount

Interactive examples were hard-wired to ascent: `Example.body` is a
`URIO[R & Scope, ascent.ast.UI[R]]`, and the docs client ran that body then
called `AscentApp.mount`. A library rendering through anything else could not
have a live example at all. The concrete casualty was preactile, whose docs
client is a deliberate `ZIO.never` stub.

An interactive example is now a keyed DOM mount. `SiteBuilder` stamps
`data-specular-mount="<key>"` on each interactive example's wrapper, and the
Scala.js `SpecularClient.mountAll` scans for exactly that attribute and
dispatches on the key. Ascent stops being the mechanism and becomes one
adapter (`Mounter.fromAscent`), wired automatically by `.interactive`, so
preact, laminar, slinky, tyrian and raw DOM are all first-class.

Source panels come from real files, not strings. The mount code lives in a
Scala.js project the JVM DocSpec cannot see, let alone typecheck, so
`exampleDom(key).fromSource(path[, marker])` names a file and the JVM-only
`DomSourceLoader` reads it at build time. Nothing a reader sees was hand
retyped, and a moved file fails the build instead of silently rotting.

Three client behaviors are load-bearing, each a bug in the obvious
implementation: mounters share the page's Scope (a per-mount `ZIO.scoped`
releases listeners at setup), each mount is `.exit`-guarded including defects
(one bad example must not blank the page), and mounts are forked (a
`ZIO.never` mounter must not starve the rest).

`DomExample` is a documented exception to the "only `.assert` makes a test"
asymmetry: it always emits a source-resolution test, because its correctness
depends on the filesystem and a deleted marker has to go red under a plain
test run.

Security posture matches the existing invariants. `DomSourceLoader` is the
read-side counterpart to `SiteBuilder.writeUnder`, one step stronger: reads
are confined to `specularSourceRoot` by real-path comparison, so an absolute
path, a `..` escape, a symlink out of the tree, and a case-only mismatch
(which would pass on macOS and break Linux CI) are all rejected, and files
over 64 KiB are refused rather than inlined. `MountKey` restricts keys to
`[A-Za-z0-9._-]+` and throws at construction, since a key reaches an HTML
attribute; the client error box uses `textContent`, never `innerHTML`.
`validatePages` fails the build on duplicate keys across the whole site,
because the client keys one Map.

Also fix the CI gate this rode in on: `ciVerify` ran plain `test`, which is
`testQuick` on sbt 2 and prints "No tests to run" while proving nothing. It
now runs `testFull`; .github/workflows/ci.yml is regenerated, not hand-edited.

Verified: scalafmtCheckAll green; 52/32/11/94/35 tests pass for
core/coreJS/zioTest/site/docs; docs/specularSite writes the site. In the
browser, the raw-DOM counter mounts and counts up, both pre-existing ascent
interactives still remount, all six mount points across five pages mount with
a clean console, and misspelling a key produces the visible error box plus a
console error.

* Give the live-server spec headroom on a cold CI runner

Switching Verify from `test` to `testFull` surfaced SiteServerSpec's live
server case, which `testQuick` had been skipping in CI. It boots real Netty
plus a ZClient under a 5 s timeout: ~2.5 s warm locally, over budget on a
cold runner. The timeout is a deadlock guard, not a performance bound, so
give it 10 s.