Changelog
This root file carries the release stories. As of create-stator 1.2.0, mechanical per-package changelogs are generated by changesets into each package’s own CHANGELOG.md; the process lives in RELEASING.md. A story appears here when a release has one — minors with an arc; the per-package changelogs are always the complete record.
Versioning: the 0.9 line is the release-candidate surface for 1.0. Per the project’s versioning decision, 1.0.0 ships only after the proving demo validates the API without breaking changes. Subpaths server, machine, template, client, dev, build, and components are treated as stable from 0.9.0; compiler and vite are internal and may change in minors.
@statorjs/stator 2.8.0 — 2026-08-27
Section titled “@statorjs/stator 2.8.0 — 2026-08-27”The serving layer learns to cache. Static responses carried no caching headers at all, so every repeat request paid for the full body. Now the framework’s hashed-output namespace — /static/assets/*, island bundles and emitted URL assets — ships immutable for a year, safe because a content-addressed URL can never change its bytes, and every other static file answers revalidation with a bodyless 304 (ETag + Last-Modified), so a font or stylesheet costs its bytes once per actual change. AVIF images and TTF/OTF fonts get real content types instead of octet-stream. The dev servers keep serving no-cache — an edit always shows.
Also surfaced: catch-all route params. routes/media/[...path].ts matches zero or more segments with the raw remainder in params.path — they have worked since route discovery shipped, but neither the docs nor the source ever said so; now both do, with the first integration test. And the island-bundling seam gained its second implementation: STATOR_ISLAND_BUNDLER=esbuild bundles islands through esbuild with code splitting — experimental and off by default, with the measurements that make it the Vite exit’s endpoint candidate recorded in the toolchain spec.
@statorjs/stator 2.7.0 — 2026-08-25
Section titled “@statorjs/stator 2.7.0 — 2026-08-25”The dev inspector now inspects machines, not just wire traffic. The toolbar gains a Machines tab: a nav listing every machine with its live state inline — your session’s machines first, then the process-global app machines, then the route table with each route’s reads — beside a detail pane showing the selected machine’s context and the events its current state accepts, server-only and guarded ones marked. It is a navigable graph, not two lists: a route’s reads chips jump to that machine’s detail, a machine’s detail names the routes that read it, and static GET routes render as real links. And when a persisted snapshot was written by different machine code, a stale chip says the session starts fresh on its next request — the working-state policy made visible instead of inferred from logs.
Behind the tab are two additive pieces of public surface. describeMachine(def) serializes a machine definition to plain data — states, transition candidates with their guards and effects, after timers, serverOnly, emits, selectors, reads — with closures reported as presence, never bodies; it is the substrate later tooling (chart prints, visualization, a build-time manifest) reuses. And the dev servers serve GET /@stator/inspect: the catalog plus snapshots, scoped to the caller’s own session cookie by construction, and read-only — no actor instantiated, no session lock taken, nothing dispatched. Production never registers the endpoint, even when a site opts into the wire toolbar, because machine context is working state and may hold anything.
@statorjs/stator 2.6.0 — 2026-08-24
Section titled “@statorjs/stator 2.6.0 — 2026-08-24”Development now runs your app the way production does. stator dev executes straight from the source tree under Node’s module loader — .stator files compile on import, islands bundle behind the same seam the production build uses and are served from memory on the production URL shape — so there is no second module graph, no transform that exists only in dev, and an import.meta.url-relative path (a SQLite file, a data dir) means the same thing in both worlds. An edit re-evaluates exactly the changed modules and their transitive importers, so a lib/db.ts that opens a connection at top level runs once per session, not once per edit; a failed rebuild keeps the last good build serving with the compile error, code frame and all, in an overlay. Server stack traces and dev island bundles are source-mapped. STATOR_VITE_DEV=1 keeps the previous Vite-embedded dev server for one minor as an escape hatch, and DevApp.vite is deprecated — the dev server no longer embeds Vite.
Machine state also got its policy: sessions never outlive the code that made them. Every persisted snapshot is stamped with a hash of its machine’s code — the machine file plus every module it reaches — computed at stator build into the manifest (an unbundleable machine fails the build, not a production boot) and live in dev. Hydration discards a snapshot whose hash no longer matches, so a renamed state can’t strand a session and a deploy resets exactly the machines it changed — stator build prints which. This release resets all persisted machine state once, since existing snapshots carry no hash. For the durable facts that outlive working state, session-machine effects now receive meta.session (session id + claims): reload what matters by identity in an entry effect, no client round trip.
@statorjs/stator 2.5.1 — 2026-08-19
Section titled “@statorjs/stator 2.5.1 — 2026-08-19”A dev/prod parity patch: the dev server no longer reads a user vite.config.*. Production always ignored it, so a plugin configured there ran under stator dev and silently vanished from the build — an app that worked in dev and shipped broken. Both sides now agree.
@statorjs/stator 2.5.0 — 2026-08-18
Section titled “@statorjs/stator 2.5.0 — 2026-08-18”Deploy-aware clients, and the seam for server-lifetime work. A live page now carries the build it was rendered against, and if it reconnects to a server on a newer build — a dev restart, a production deploy — it hard-reloads instead of resyncing onto a slot map that may no longer match, closing both the dev restart-without-reload gap and post-deploy staleness. And boot.ts (defineBoot) is the home for a long-lived inbound source: query config at startup, then run a poll or subscription that feeds the app-machine graph, with a teardown composed into graceful shutdown. Its BootContext is deliberately narrow — dispatchToApp and read-only config, never the raw app — because boot is a source, not a controller: cadence and policy live in the machine’s guards, where they stay testable. The CLI took over the createApp call in 2.2, which is exactly why this seam had to be first-class rather than a hand-written server.ts helper.
@statorjs/stator 2.4.0 — 2026-08-18
Section titled “@statorjs/stator 2.4.0 — 2026-08-18”Secrets and config, and part two of the auth-primitive thread. Stator now loads .env/.env.local into process.env at startup (real environment wins), so the same code reads config in dev and prod — no import.meta.env, no shell-export ritual, no dependency (Node’s own loadEnvFile). On that foundation, signed cookies (cookies.setSigned/getSigned) are the sealed short-lived-state primitive for auth handshakes — an OAuth state, a magic-link token, a WebAuthn challenge — signed with an app secret so the server can hand state to the browser and trust it on the way back, no per-attempt database row. A tampered or since-rotated cookie reads as absent. Substrate for a third-party auth toolkit, not an auth system Stator ships.
@statorjs/stator 2.3.0 — 2026-08-17
Section titled “@statorjs/stator 2.3.0 — 2026-08-17”The security round — four focused changes, one thesis: give third-party auth its primitives instead of shipping an auth system. Composable cross-site write protection (crossSiteGuard, trustedOrigins, a Strict posture); a middleware.ts seam with framework security defaults that run first and cors()/securityHeaders() as primitives; and the session-identity substrate — claims as a minimal identity projection the machine-unaware edge can gate on, rotateSession/clearSession lifecycle ops, an establish-once session. The last piece, serverOnly, lets a machine declare the events no client may send, so a forged CHARGE_APPROVED is rejected at the wire, not processed as truth. The with-auth starter and the storefront cart proved it. The lesson, learned from every framework that tried to build auth generically and abandoned it: enable the toolkits, don’t become one.
@statorjs/stator 2.2.0 — 2026-08-15
Section titled “@statorjs/stator 2.2.0 — 2026-08-15”Stator starts owning its toolchain. A stator CLI (dev/build/start/check/test) replaces the hand-written server.ts/build.ts/start.ts an app used to wire itself, and stator build runs stator check first — a full server-stack typecheck, not just islands — so a broken server import fails the build instead of shipping silently. Configuration moves into a first-class stator.config.ts (defineConfig), grouped by concern (persistence, sessions, realtime, dev, port), every field optional; the old flat createApp options still work, now deprecated. Production also got quieter — createApp defaults to warn and the per-request/per-connection lines dropped to debug, while the startup notice always prints. The CLI is Phase 0 of the owned dev/build pipeline.
@statorjs/stator 2.1.0 — 2026-08-11
Section titled “@statorjs/stator 2.1.0 — 2026-08-11”Islands get a server-side frontmatter fence. An island file may now carry a frontmatter block that runs per shell render — a server component’s contract — with its bindings in scope for the template and invisible to the <script> in either direction. It’s the home for server work the island owns (imports, computed constants, queries); per-use data stays props. Route-only markers (Stator.*) and a fence binding that collides with a use() field are located errors, so the seam is explicit, not magic.
@statorjs/stator 2.0.0 — 2026-08-09
Section titled “@statorjs/stator 2.0.0 — 2026-08-09”The removal release. Two-way binding was the one place a Stator machine changed state without a declared transition — the compiler-made @set event skipped guards, skipped types, and could not be seen in the machine’s own definition. 2.0 deletes it, and with it the split personality of the display layer: read() is now the single display primitive on both sides of the boundary, lowered by the machine’s location, and on: with typed events is the only way state changes. The directive surface ends at on: and ref:.
Forms follow a pattern instead of a primitive. The input owns its draft with the platform as its guard, a typed event commits it at a real boundary, and pre-fill is a server-rendered attribute. The registration starter earned this release — built to prove the pattern, it found three framework bugs in its first day (all seam disagreements, all fixed), settled two doctrine questions, and its paper-cut log made the final call: no replacement sugar needed. The measured case for removal was never subtle — one real two-way binding existed in the entire codebase.
Also gone: the deprecated one-bag machine() form. Also kept, deliberately: the wire’s rejection of @-prefixed events, so the reserved namespace stays unreachable from untrusted input forever.
@statorjs/stator 1.9.0 — 2026-08-09
Section titled “@statorjs/stator 1.9.0 — 2026-08-09”The typed-component release, and the last minor before 2.0. A component can extend a native element: Stator.props<HTMLAttributes<'button'> & { variant }>() types every native button attribute plus the component’s own props, {...rest} spreads them onto the inner element — static values and live read() bindings both — and on: directives forward through Stator.forwarded('on:click'), with placement left to the component author rather than forced onto the root. JSX.IntrinsicElements is typed per element now, so a typo on a plain <button typ=…> is a compile error while islands, raw() SVG, and unlisted tags stay permissive. And a machine instance in a template finally types both halves of its contract: state narrows to the state-name union and send() checks against the event union, so a bad state name, event name, or payload fails to compile instead of slipping through as a string.
@statorjs/stator 1.8.0 — 2026-08-01
Section titled “@statorjs/stator 1.8.0 — 2026-08-01”The release where the framework stopped touching your DOM. Reactive regions (each/when/match/defer) are delimited by HTML comment markers now instead of a wrapper <span style="display:contents"> — so a reactive each of <tr> inside <tbody> renders (the parser used to foster-parent the span out of the table and break the list), and CSS sibling/child selectors match the elements you wrote, with no injected node between them. The fix arrived with the repo’s first real-browser test harness, because the entire existing suite ran on a DOM that doesn’t implement table parsing and was structurally blind to the bug. Machines also gain a top-level on: — any-state handlers consulted when the current state doesn’t declare the event, the home for a completion event that must not be dropped just because the machine moved on. And JSX inline whitespace follows JSX’s own rules: {count} unsaved keeps its space.
@statorjs/stator 1.7.0 — 2026-07-29
Section titled “@statorjs/stator 1.7.0 — 2026-07-29”The dogfooding release: a real app was rebuilt on Stator, every point of friction was logged, and this release is the framework’s answer to that log. The headline is that GET grew up.
- Data GET routes:
defineApiRoute({ method: 'GET', reads, handler })declares a read-only data route — the handler receivesmachines(read proxies keyed by machine name, the same shape a page render context uses) and structurally nodispatch, which is exactly what makes handler reads safe. Machines hydrate under the session lock; the lock releases before the handler runs. A plain return value is JSON; a string takes itsContent-Typefrom the URL’s extension (routes/feed.xml.tsserves/feed.xmlasapplication/xml, plus.txt,.ics,.csv); a rawResponsepasses through verbatim. Synthesized responses carry a strongETagand answerIf-None-Matchwith a bodyless 304, so polling consumers stop paying for unchanged data. - Param segments compose with extensions:
routes/p/[id].json.tsserves/p/:id.json— the captured param excludes the literal suffix, and the suffixed route outranks a bare/p/:idpage, so a poll page and its JSON twin coexist at one URL. Found the same day the live-poll example dogfooded data routes; that is the method working. dispatchToAppas a method on bothStatorAppandDevApp— the server-originated dispatch plane (webhooks, cron) no longer needs astorethe dev server never exposed. The dev method follows the current store across rebuilds and runs through the Vite-loaded runtime, so SSE fan-out reaches live connections instead of a second module instance’s empty registry.- Four documented types are now importable:
EntryEffectandAfterEntryfrom@statorjs/stator/machine,DispatchResultandDispatchErrorfrom@statorjs/stator/client.
- A route file exporting an HTTP-method name with the wrong constructor now errors at discovery with the existing hint, instead of being silently skipped as a utility file (the skip surfaced as an unexplained 404 and a dev banner counting one route short).
- A raw
Responsereturned from an API route handler is recognized by shape, not onlyinstanceof— cross-realm and wrapped Responses pass through — and a return value that is neither aResponsenor a{patches, directives}envelope logs a warning instead of silently becoming an empty envelope.
Examples
Section titled “Examples”Three examples dogfood the new surface: the guestbook serves its book as an RSS feed at /feed.xml (visitor text XML-escaped), with-auth serves its notice board as viewer-gated JSON at /api/notices (members-only notices absent from a visitor’s body, not hidden in it), and live-poll serves per-poll results at /p/:id.json beside the live page — the same machine, pushed to browsers over SSE and polled by programs with 304s between votes.
create-stator 1.5.1 — 2026-07-29
Section titled “create-stator 1.5.1 — 2026-07-29”- Scaffolded apps now declare
@statorjs/stator ^1.6.0(the range had trailed at^1.4.0since 1.5); a CI check keeps the scaffold range in step with the framework from now on.
@statorjs/stator 1.6.0 — 2026-07-27
Section titled “@statorjs/stator 1.6.0 — 2026-07-27”The connectivity release: the wire stopped pretending networks don’t fail. Every machine-event POST now carries a client-generated idempotency key — the server replays a duplicate’s response verbatim instead of re-applying it — so the client can retry network failures and timeouts with backoff, and a tap on flaky wifi recovers instead of silently dropping. Dispatch signals its state (data-stator-pending on the dispatching element, a connection attribute on <html>, a 10-second deadline, an error event for anything that still fails), and a dropped or stale SSE channel resyncs the page in place — scroll, focus, and island state survive where a reconnect previously forced a reload. The 1.5.2 patch had made zombie connections observable (heartbeats as data frames); 1.6 made recovering from them invisible.
@statorjs/stator 1.5.0 — 2026-07-26
Section titled “@statorjs/stator 1.5.0 — 2026-07-26”The hardening release for two earlier promises. Templates became provable: syncTypes emits each template’s virtual TSX so plain tsc --noEmit catches frontmatter and prop errors in CI that previously surfaced as runtime ReferenceErrors — the same analysis the editor runs, now a gate. And state-anchored work got its lifetime contract: entry effects are the load role (re-invoked if a process died mid-flight, abortable on state exit), transition effects are the command role (at-most-once, never re-invoked), and after timers re-arm on hydration with elapsed credit — a restart no longer silently kills a countdown.
@statorjs/stator 1.4.0 — 2026-07-23
Section titled “@statorjs/stator 1.4.0 — 2026-07-23”Async data on pages — the top gap in the primitive analysis — landed as defer(thunk, { ready, error }): an async region resolved outside the synchronous render, every defer on the page awaited in parallel, rendered inline. Frontmatter stays synchronous, the permanent contract. defer is the one-shot door; a machine with a loading entry effect is the reactive one, and the compiler enforces the line (a machine read inside a defer arm is a build-time error). Rows got fine-grained liveness too: read(item, …) inside an each patches a single field in place — text or attribute — across keyed moves, without re-rendering the row.
@statorjs/stator 1.3.0 — 2026-07-16
Section titled “@statorjs/stator 1.3.0 — 2026-07-16”States learned to cause work, not just receive it. An entry effect fires when a state is entered — the trigger the reactive-load pattern needed: a machine starts in loading, fetches in its entry effect, moves to ready or error, and live pages see each step over SSE. after state timeouts are the companion (armed on entry, cancelled on exit, guard-dropped if the state moved on) — the rescue for a state whose effect never completes. Both work on app-lifecycle machines with no session attached, which is what self-revalidating caches and circuit breakers are made of.
@statorjs/stator 1.2.0 — 2026-07-13
Section titled “@statorjs/stator 1.2.0 — 2026-07-13”The security minor, driven by building the auth example. rotateSession() is the session-fixation defense: on login the whole session moves to a freshly minted id, and { clear: true } on logout deletes the old state outright. Every server dispatch surface also began reporting { committed }, so a login handler can tell a guard-dropped event (wrong credentials) from a committed one. The 1.2.1 patch followed a day later with a sweep across every layer — most notably closing the wire’s reserved @set event, which could previously write arbitrary context past every guard, plus static-file containment, URL-scheme sanitization, and CSRF origin checks.
@statorjs/stator 1.1.0 — 2026-07-12
Section titled “@statorjs/stator 1.1.0 — 2026-07-12”Post-launch polish, driven by real editor use and public-demo feedback. (The published 1.0.0 predates the typed-client work below; inspector was in the repo but is first published here.)
- Typed client machines:
machine(context, behavior)— context and behavior are separate arguments so handlers and selectors infer real context types, anduse()instances expose context keys + selector results as typed properties (this.sel.colortype-checks; typos are compile errors). The one-bag form still works, deprecated and loosely typed — one bag is untypeable by construction.bind/effectaccept any instance via the newClientInstanceBasesurface. - Production inspector:
createApp({ inspector: true })serves + injects the wire-inspector toolbar (the live demo runs it). - Dev server manners: startup banner (clickable local/network URLs, version, machine/route inventory) and graceful SIGINT/SIGTERM shutdown — Ctrl+C exits 0 instead of an ELIFECYCLE error; prod deploy rollovers exit clean too.
- Compiler: the language-server virtual emit injects only template/client globals the author didn’t import (fixes false “Duplicate identifier ‘raw’” editor diagnostics); client-component CSS scopes by descendant of the root, so runtime-created island DOM matches scoped styles.
create-stator 1.1.0 — 2026-07-12
Section titled “create-stator 1.1.0 — 2026-07-12”- Interactive scaffolding (clack): directory prompt, template selection plumbing, spinner, next-steps outro. Fully non-interactive when arguments are passed (
--template,-h). - Starter fixes every new project needed: tsconfig now claims
**/*.statorwithjsx: preserveandDOM.Iterable(without these, the editor types.statorfiles under an inferred project — “cannot find StatorElement” phantom errors); a README with the mental model; templates live undertemplates/minimalready for siblings.
@statorjs/language-server 0.1.1 — 2026-07-12
Section titled “@statorjs/language-server 0.1.1 — 2026-07-12”- Rebundled with the user-aware import injection (duplicate-identifier fix) and the descendant CSS scoping — this package is the LSP binary editors other than VS Code run, so it needed the same fixes the VS Code extension got in 1.0.3.
@statorjs/stator 1.0.0 — 2026-07-08
Section titled “@statorjs/stator 1.0.0 — 2026-07-08”The proving-demo release: everything the storefront demo (demo.statorjs.dev) forced the framework to get right. The 0.9 API survived a real application without breaking changes; these are the hardening fixes and the features it earned along the way.
- Reads-aware selectors: selectors receive the same
{ reads }helpers as actions/guards, so cross-machine verdicts project as display state — and bindings on the reading machine re-diff when a read machine changes. - Boolean attribute bindings:
disabled={read(…)}renders absent for falsy, present-and-empty fortrue; attr patches carryvalue: nullfor removal (wire change, pre-publish). dispatch()returns{ ok, committed, patchCount }— a guard-dropped event isok && !committed; the POST envelope carriescommitted.- Island attribute reactivity: declared attrs are observed; changes invoke coerced
${key}Changed(next). Island props acceptread()— live server-bound attributes. - The hydrate pattern, pinned: island templates render server sections from props (maps with nested JSX, component renders as props,
read());html\“ splices fragment arrays. - Production inspector:
createApp({ inspector: true })serves + injects the wire inspector toolbar. - Emit-cascade guards: wire-time subscription-cycle warning, runtime depth cap with a named trail, circular-import diagnosis.
- Live views: initial-sync on SSE connect (pages that missed changes mid-navigation converge); fan-out rehydrates session actors from the Store and scopes session touches to the owning session; the dispatching page’s own connection is skipped (double-insert fix).
- Wire contract: route keys carry the page’s query string into baseline re-renders; branch arms scope their slot ids (
s2:btrue:s0) so stale pages skip instead of miswrite; the applier warns on missing targets. buildAppmirrors the app’s source tree (machines importinglib/no longer break production builds).- Client components scope CSS by descendant of the root — runtime-created island DOM matches scoped styles.
- Touched means committed: guard-dropped events no longer persist, fan out, or report
committed: true.
- Testing guide (the inverted pyramid), divergence contract, islands-are-leaves channels, command-endpoint API routes, mutual-machine wiring, 404 idiom, converging completions, reads-aware selectors.
@statorjs/stator 0.9.0 — 2026-07-04
Section titled “@statorjs/stator 0.9.0 — 2026-07-04”The “everything since the POC” release: the 1.0 feature surface, complete.
Engine & machines
Section titled “Engine & machines”- Custom isomorphic state-machine engine (replaces XState): flat typed state graphs,
to/when/do/emittransitions, mutation-syntax actions overstructuredClonedrafts, declared emits, snapshot persistence. - Async effects:
effect: async (ctx, ev, { effectId }) => Events | nullon transitions — host-scheduled after commit, never under the session lock; completions re-enter the normal event path and reach live pages over SSE. Works on session machines, app machines, and client islands. At-most-once, non-durable (durability is 1.x). - Typed machine-mediated dispatch: machines addressed by imported def, events checked against each machine’s declared union, server and client.
- App-machine persistence (
persist: true) through a newAppStoreinterface (InMemoryAppStore,RedisAppStore); boot hydration with log-loud-start-fresh recovery. dispatchToApp: typed server-originated dispatch for webhooks and cron — send, persist, fan out to live connections.createAppnow exposesstorefor it.
Templates & rendering
Section titled “Templates & rendering”.statorsingle-file components (TS frontmatter + JSX template + scoped CSS), compiled to server render modules; file-based routing with path params,// @stator livepragma, GET-.stator+ POST-.tsroute merge.- Keyed lists:
each(items, fn, { key })emits per-iteminsert/remove/movepatches from a server-side diff; rows keep identity (focus, transitions) across reorders; key-derived slot scopes. - Wire protocol consolidated into one shared module (
src/wire/) both sides typecheck against; documented in WIRE.md.
Client islands
Section titled “Client islands”- Whole-file custom elements (structural detection, no pragma):
use()with hydration seeds, two-waybind:, typeddispatchto server machines. - Production island builds:
buildAppbundles islands via Vite into hashed assets with a route→island manifest;loadProductionHeadinjects per-route scripts. Server-machine imports collapse to{ name }identity stubs in browser bundles (dev and build).
Server & ops
Section titled “Server & ops”- SSE live routes with cross-session fan-out (single replica); per-session store adapters (
InMemoryStore,RedisStore, write-throughCachedStore). - One shared per-session lock across
/__eventsand API routes (fixes a cross-path lost-update race). - Hardened
/__eventsinput handling; automatic HTML escaping verified against XSS payloads; session cookie flags (HttpOnly,SameSite=Lax,Securegating) under test. @hono/node-servermoved to dependencies (was devDependency — broke real consumers);pino-prettyis now optional with a graceful JSON fallback.
Tooling & DX
Section titled “Tooling & DX”create-statorscaffolder (pnpm create stator my-app).- Vite-embedded dev server: live reload, compile-error overlays with code frames, auto-injected client runtime and dev inspector.
- VSCode extension + Volar language server for
.stator(highlighting, TS/CSS intelligence across regions). - Docs site: tutorial (9 chapters), concepts, guides, hand-written API reference.
@statorjs/language-server 0.1.0 — 2026-07-04
Section titled “@statorjs/language-server 0.1.0 — 2026-07-04”- First versioned cut: Volar language plugin + Node server; TS and CSS services federated across
.statorregions.
create-stator 0.9.0 — 2026-07-04
Section titled “create-stator 0.9.0 — 2026-07-04”- Initial release: prompt-free scaffold with a complete working app template (dev/build/start/sync wiring, counter machine,
.statorpages).