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/api 0.5.0 — developer experience#

No runtime change — an upgrade from 0.4.1 is drop-in. With TypeScript, a URL written as a string literal now types its params: api("/users/:id", { params: { userId } }) and api.patch("/users/:id").run({ name }) are caught in your editor, not only at runtime. Existing typed code keeps compiling.

@zoijs/api 0.4.1 — cancellation fix#

Fixes a narrow race in authenticated clients: a mutation reset() (or unmount) in the moment between the auth provider answering and the request starting could still send the request. The provider is also no longer asked for a token for a request that was already cancelled. No API changes — upgrade from 0.4.0.

  • Recovering from a 401: the documented pattern now recovers at most once until a request succeeds, so a server that keeps answering 401 can't cause a refresh loop.
  • Redaction limits are documented: only exact copies of the token are replaced.

@zoijs/api 0.4.0 — cancellable auth providers#

The auth provider now receives { signal }, which aborts when its request is no longer needed — superseded, disposed, unmounted or reset — and the request stops waiting at once. An optional authTimeout gives up on a slow provider (an "auth" error, nothing sent). See Authenticated clients.

  • Echoed tokens redacted: if a server puts the request's own token into its problem details or status text, it's replaced with [redacted] before reaching errors or monitoring.
  • Recovering from a 401 is documented: refresh your session yourself and retry once — Zoijs still never retries a 401.
  • 0.3.0-style providers keep working; api() and clients without auth are unchanged.

@zoijs/eslint-plugin 0.4.1 — security fix#

Fixes a regular-expression denial of service in alt-text, no-dynamic-style, no-static-element-interactions and no-target-blank-without-rel: an unclosed tag full of quote characters in an html template could make ESLint hang. Matching is now linear. Upgrade from 0.4.0. See ESLint plugin.

@zoijs/api 0.3.0 — Bearer authentication for API clients#

createApi({ baseURL, auth }): an optional provider (sync or async) returns the access token, and Zoijs sends Authorization: Bearer <token> — only to that client's origin, with cookies still omitted and redirects never followed. See Authenticated clients.

  • Once per request: asked for each load, refresh() or run(), never per retry — retries reuse the same token. null/undefined sends the request anonymously.
  • Validated and private: tokens must use RFC 6750 characters; a bad token or a failing provider is an ApiError of type "auth", nothing is sent, and errors never contain the token.
  • No automatic refresh: 401 and 403 aren't retried; api() and clients without auth are unchanged.

@zoijs/api 0.2.0 — explicit clients for external APIs#

createApi({ baseURL }) creates a client for one explicitly trusted external origin, with the same surface and options as api(). api() itself is unchanged and stays same-origin-only. See External APIs.

  • One origin per client: paths resolve under the base path; full URLs, //host, backslashes and .. segments are refused, and redirects are never followed.
  • Browser protections stay on: CORS still applies, no cookies or credentials are sent, the referrer never carries your page's path, and no authentication is added.
  • Published from CI with npm Trusted Publishing and provenance.

@zoijs/api 0.1.0 introduced the package: same-origin reads as a resource, writes as an action, with timeouts, problem details, idempotency keys and bounded retries. See Same-origin HTTP.

@zoijs/core 1.9.0 + 13 package releases — Phase 1 security hardening#

The largest security and correctness release so far. Documented usage is unchanged; code that relied on undocumented, unsafe behavior may need the short migration steps in the core 1.9.0 release notes.

  • Security: template results are Symbol-branded (data can't forge them); html only accepts tagged templates (html([...]) throws ZJS010); bound srcset, meta refresh and SVG animation URLs are checked; a bound <base> is refused; target="_blank" always gets rel="noopener noreferrer". Raw HTML has one explicit opt-in, unsafeHTML() from @zoijs/core/unsafe. See Security.
  • Production: the new @zoijs/core/prod entry starts in production mode, and configure({ onError }) reports the errors Zoijs contains. See the Production Security checklist.
  • Correctness: compatible copies of the core share one runtime (and one Trusted Types policy); a conditionally shown component returned uncalled no longer rebuilds; template compiler edge cases fixed.
  • Packages: @zoijs/ssr 0.5.0 (needs core ^1.9.0), @zoijs/router 0.6.0 (decodeSlash, app-paths-only go()), @zoijs/action 0.2.0 (exclusive), @zoijs/storage 0.2.0 (validate), @zoijs/sanitize 0.2.0 (id namespacing against DOM clobbering), @zoijs/resource 0.3.0, @zoijs/i18n 0.2.0, @zoijs/eslint-plugin 0.4.0 (no-unsafe-html, no-html-call), create-zoijs 0.2.0, plus documentation patches for head, forms, devtools and testing.
  • Supply chain: every package is now published from CI with npm Trusted Publishing and provenance; CDN guidance uses exact versions with integrity hashes. See Installation.

@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 form — style=${() => ({ … })} — 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.