Changelog

Published releases of Zoijs. The core follows semantic versioning; optional packages are pre-1.0 and may still evolve. Full notes live on the GitHub Releases page.

@zoijs/sanitize 0.1.0 + @zoijs/core security hardening#

A new optional package, @zoijs/sanitize, turns an untrusted HTML string (markdown or CMS output) into safe DOM nodes for a text binding — the supported alternative to a raw-HTML sink, which Zoijs deliberately doesn't have. It parses inertly with DOMParser, prunes against an allowlist, scheme-checks URLs with the core's own isSafeUrl, and hardens target="_blank" links. See the Sanitize page.

Alongside it, two core hardening changes: the <object data> URL sink is now scheme-checked like href/src (it was missing from the URL-attribute allowlist), and style gains an injection-safe object formstyle=${() => ({ … })} — applied per-property so a value can't break out of the attribute or inject extra declarations. A dynamic style string still works but earns a one-time dev warning on risky tokens. See the Security page.

@zoijs/resource 0.2.0 + @zoijs/ssr 0.3.0 — server→client data hand-off#

A small pair that lets server-rendered data reach the client without a refetch or a loading flash. resource(fetcher, { initial }) starts a resource already-settled with a value instead of auto-loading, so a hydrated resource keeps server data (the presence of the initial key — not its value — skips the load; refresh() still works). @zoijs/ssr adds serialize(value), a JSON serializer that is safe to embed in a <script> (it escapes <, >, &, and the U+2028/U+2029 line terminators, so a </script> in your data can't break out). The server renders with the value and embeds serialize(data); the client seeds the resource with it. No async SSR, no loaders — the two are the data primitive. See the Server Rendering page.

@zoijs/head 0.1.1 + @zoijs/router 0.2.1 — SSR-safe#

Both packages now run under @zoijs/ssr's renderToString instead of throwing. head's title() / description() / meta() are no-ops when there is no document (put the initial tags in your HTML shell; head updates them after hydration). The router's createRouter() and view() no longer touch window / document on the server — view() renders the matched route's template directly (the / route, since no request URL is available server-side) and navigation becomes a no-op until the client hydrates. No API change; per-request routed SSR remains a separate, planned step.

@zoijs/eslint-plugin 0.1.0#

A new optional package: lint rules for Zoijs. One small, auto-fixable rule — require-reactive-binding — flags a reactive .get() read inside an html template that isn't wrapped in () => (read once during setup, never updates — the one footgun with no runtime signal) and rewrites it for you. Narrow by design: only zero-argument .get() inside html templates, never .peek(), and never a read deferred behind a nested function. Ships recommended (flat config) and legacy-recommended (.eslintrc) configs, with zero runtime dependencies. See the ESLint Plugin page.

@zoijs/ssr 0.2.0 + @zoijs/core 1.6.0 — full SSR (hydration)#

Hydration lands, completing server-side rendering. The client now adopts the server-rendered DOM in place instead of re-creating it: hydrate(App, "#app") (from @zoijs/ssr) reuses the existing elements exactly and attaches their events and reactive attributes to those live nodes — dynamic content re-renders into the existing structure with no full re-render and no flash. Render the markup with renderToString(App, { hydratable: true }) so the client can find and reuse it. Under the hood this is the core's additive mount(App, target, { hydrate: true }) (core 1.6.0); the nine-function main surface is unchanged. See the Server Rendering page.

@zoijs/router 0.2.0#

Adds an interceptLinks option. createRouter(routes, { interceptLinks: true }) makes a plain left-click on any internal <a> — not just a router.link() — navigate client-side instead of doing a full page reload. This is what lets links inside rendered content (Markdown cross-references, a CMS body) feel like a true SPA, with no flash. It bows out for modifier / new-tab clicks, target, download, external origins, other schemes, same-page #hash links, links outside the base, and any <a data-native>. Off by default. (This site now uses it.)

@zoijs/ssr 0.1.0#

A new optional package: server-side rendering. renderToString(component) renders a Zoijs component to an HTML string with no DOM and zero dependencies — for SSR (first paint + SEO) and static prerendering (SSG). The same component code runs on the server and the client; on the client, mount takes over. It reuses the exact security predicates the browser renderer uses, so server and client output make identical decisions. Seamless DOM-adopting hydration is a planned future core capability. See the Server Rendering page.

@zoijs/core 1.5.0#

The template compiler is now DOM-free: html\…\` compiles to a static HTML string + part descriptors without touching the DOM (the <template> is built lazily on first client render), so a component can be evaluated on a server. A new @zoijs/core/server subpath exposes the building blocks a string renderer needs — including the **same** escaping and URL-safety predicates the client uses, so [@zoijs/ssr`](ssr.md) makes identical security decisions. Client rendering is byte-for-byte unchanged, and the nine-function main surface is the same. See RFC 0008.

@zoijs/i18n 0.1.0#

A new optional package: reactive internationalization. createI18n() gives you a reactive locale, a message lookup with {placeholder} interpolation and dotted keys, and plurals chosen by Intl.PluralRules (so every language's rules are correct for free). n() / d() / list() wrap the platform's Intl formatters in the current locale, and setLocale() updates every binding in place — no provider, no context, no re-render. Zero dependencies; translations are inert text when bound. See the Internationalization page.

This is the one package shipped in Phase 7 (ecosystem fill). Three other candidates were deliberately declined and redirected to the platform: an HTTP client (use fetch with @zoijs/resource / @zoijs/action), an animations package (an Animations recipe — CSS + the Web Animations API via ref), and an icons package (an Icons recipe — plain SVG). A small, sharp ecosystem is the point.

@zoijs/core 1.4.0#

Adds a devtools inspection hook (@zoijs/core/devtools) — a dev-only, read-only seam an inspector can attach to in order to observe the reactive graph: states, computeds, effects, the edges between them, and which DOM node each binding updates. It rides a dedicated subpath, so the learnable nine-function main surface is unchanged. The hook is off by default (a single null check until something attaches), never instruments the hot read path, and is a no-op in production mode — so a shipped app pays nothing and exposes nothing. See the DevTools page.

@zoijs/devtools 0.1.0#

A new optional package: a reactive-graph inspector. Call inspect() for a floating, dev-only panel that lists every state, computed, and effect, flashes the node that just changed, and outlines the single DOM node a signal updates — the fine-grained model, made visible. createInspector() exposes the headless model (nodes, edges, stats, change events) for custom UIs, extensions, or tests. Read-only, zero-dependency, no time-travel (Zoijs has no re-render to replay). See the DevTools page.

@zoijs/core 1.3.1#

Performance: keyed-list each reconciliation now does the minimal number of DOM moves on reorder (a longest-increasing-subsequence pass) — moving one item across a list is a single move, and reused nodes keep their identity (focus, inputs, scroll). No API change. See the Performance page.

@zoijs/testing 0.1.0#

A new optional package: tiny first-party DOM testing helpers — render, queries (getByRole / getByText / getByLabelText / getByTestId, each with query / getAll / find variants), fireEvent, waitFor / tick, cleanup, and a mockRouter. It drives the real DOM (no custom renderer, no snapshot format) and depends only on @zoijs/core. See the Testing page.

@zoijs/core 1.3.0#

Adds boundary — a render-time error boundary. It renders a subtree, and if that subtree throws while building its markup (an error that would otherwise break the whole mount), it disposes the partial work and renders a fallback instead. Catches synchronous setup/render throws only; reactive-update errors are already contained, and async errors belong to @zoijs/resource / @zoijs/action. The core surface is now nine functions. See the Error Boundary page.

@zoijs/core 1.2.0#

Adds effect — a public reactive effect that re-runs when a value it reads changes, with a returned cleanup (before each re-run and on dispose) and owner-scoped auto-disposal. It's the public completion of the reactive trio (createState / computed / effect); the core surface is now eight functions. See the Effect page. (The svg helper considered alongside it was deferred — templates rooted at <svg> already render correctly.)

@zoijs/core 1.1.0#

Adds element refs — a ref binding that hands you the rendered DOM element (html\<input ref=${(el) => el.focus()} />\`), runnable just after insertion and with an optional cleanup on unmount or list-item removal. No new export — ref` is a binding semantic, so the core surface was unchanged at this release.

@zoijs/forms 0.1.0#

A new optional package: a native-forms-first helper that keeps a form's values, errors, and touched state in Zoijs reactive state, with a tiny validation rule map. It holds state only — pair it with @zoijs/action to submit. See the Forms page.

@zoijs/storage 0.1.0#

A new optional package: a localStorage-backed reactive value — a drop-in, persistent createState for themes, drafts, filters, and preferences. Reads on creation, writes on every set, and degrades to in-memory state when storage is unavailable. See the Storage page.

Initial public release#

The first public release of Zoijs — a stable core plus optional packages.

PackageVersionInstallnpm
@zoijs/core1.0.0npm i @zoijs/corenpmjs.com
@zoijs/router0.1.0npm i @zoijs/routernpmjs.com
@zoijs/resource0.1.0npm i @zoijs/resourcenpmjs.com
@zoijs/head0.1.0npm i @zoijs/headnpmjs.com
@zoijs/action0.1.0npm i @zoijs/actionnpmjs.com
@zoijs/storage0.1.0npm i @zoijs/storagenpmjs.com

@zoijs/core 1.0.0#

The stable, frozen seven-function core: html, mount, createState, computed, each, configure, and onCleanup. Fine-grained reactivity, keyed list rendering, owner-scoped cleanup, security defaults, TypeScript definitions, and cross-browser tests.

@zoijs/router 0.1.0#

A tiny client-side router: a plain { pattern: component } map, dynamic :params, a base path, active-link aria-current, and history-mode navigation.

@zoijs/resource 0.1.0#

The simplest async-read helper — reactive loading / data / error plus refresh(), race-safe so a slow request can't overwrite a newer one.

@zoijs/head 0.1.0#

Set the document title and <meta> from a component, with restore-on-cleanup so routed pages revert automatically.

@zoijs/action 0.1.0#

The write-side companion to resource — reactive pending / error / done / result for submits, saves, and deletes. run() never throws.

For installation details see Getting Started; for what's planned next, see the Roadmap.