1.18.0 — 2026-10-23
Version 1.18.0 lets one validation schema drive the form and the API, stops component re-renders from wiping what the user typed, makes retries safe for non-idempotent requests, and caps request bodies by default. It also ships create-bquery, closes five sanitizer XSS paths, and fixes full-page SSR templates.
It contains no API removals and no module status transitions. Three runtime defaults and two testing-helper defaults change behaviour; see Upgrading.
TIP
This page groups the changes by theme. The full list lives in the CHANGELOG. For how each API is meant to be used, follow the module guides linked from each section.
Headlines
- One schema for client and server:
createSchemaForm()and the server'svalidate()middleware accept any Standard Schema (Zod, Valibot, ArkType, …), with types inferred from it. - Re-renders keep DOM state: components patch the existing DOM instead of replacing
innerHTML, so input values, focus, caret and scroll position survive an update. - Safe retries:
httpanduseFetch()retry only idempotent methods and transient statuses by default, and honourRetry-After. - Bounded request bodies:
createServer()limits bodies to 1 MiB (10 MiB for multipart) by default, and the Node adapter no longer buffers every body up front. create-bquery: four project templates with tests wired to@bquery/bquery/testing, run withnpx --package @bquery/bquery create-bquery.
Forms and server: Standard Schema
import { z } from 'zod';
import { createSchemaForm } from '@bquery/bquery/forms';
import { validate } from '@bquery/bquery/server';
const Signup = z.object({ email: z.string().email(), age: z.number().min(18) });
// Browser: the value type comes from the schema.
const form = createSchemaForm({ schema: Signup, initialValues: { email: '', age: 0 } });
// Server: 400 with { error, issues } when the body does not match.
const signup = validate(Signup);
app.post('/signup', (ctx) => ctx.json(signup.data(ctx), { status: 201 }), [signup]);- Each issue becomes the error of the field named by its first path segment. A field's own validators run first.
- Async schemas are awaited, and the schema runs once per value snapshot.
createForm({ fields, schema })and the component-scopeduseSchemaForm()accept a schema too.validate()checks the body (JSON, URL-encoded or multipart), the query or the route params.source,statusandonInvalidadjust it.- bQuery depends on no validation library; it only calls the schema's
~standard.validate().
See Forms, Server and the recipe One schema for client and server validation.
Components: re-renders morph the DOM
Every signal or prop change used to destroy and recreate the whole shadow tree. Text typed into an <input> was wiped by any unrelated update, focus and scroll position were reset, and nested custom elements were disconnected and reconnected.
The sanitized markup is now parsed into an inert <template> and morphed into the live tree:
- Nodes are matched by tag and
data-bq-key/id, then by position, with a short lookahead so an element inserted before an unkeyed<input>leaves the input in place. - Attributes are diffed against the previous render, so attributes added or removed at runtime survive.
- Form-control properties change only when the template changes them.
- An update with identical markup skips the DOM write.
- Nested custom elements stay connected across updates. A keyed reorder that moves one uses
moveBefore()where the browser supports it (bQuery components stay mounted); elsewhere the move disconnects and reconnects it, and a bQuery component that boundon()handlers re-renders on reconnect so they keep working. - A nested
shadow: falsecomponent keeps its own light DOM.
Sanitization and the Trusted Types sink are unchanged. renderStrategy: 'replace' on the definition restores the old behaviour. See How re-renders update the DOM.
Reactive: retry policy and error bodies
A timed-out post() that the server had already processed could run up to four times with retry: 3, for example creating duplicate orders. Retries now default to:
- the idempotent methods
GET,HEAD,OPTIONS,PUTandDELETE; - the statuses
408,429,502,503and504, plus timeouts and network errors.
RetryConfig gains methods, statuses, maxRetryAfter (default 60 s) and respectRetryAfter. A Retry-After header, in seconds or as an HTTP date, replaces the backoff delay.
A response rejected by validateStatus now exposes its parsed body as HttpError.response.data, so a 422 with { errors: … } can be read in a catch. The read is bounded to 1 MiB and to the request's remaining timeout.
See Retry policy.
Server: request-body limits
createServer() limits request bodies by default:
| Key | Default |
|---|---|
json, form, text, raw | 1 MiB |
multipart | 10 MiB |
An oversized body is answered with 413 while it streams. On Node, listen() passes the largest limit to the adapter, which rejects an oversized declared Content-Length before any route runs.
The Node adapter also stops buffering every non-GET body before the app runs. It streams the body on demand with backpressure, and ctx.body() reads it once instead of up to three times. See Request body limits.
Security
sanitizeHtmlForSSR()escapes<textarea>content. Servers that register happy-dom globals renderbq-htmlthrough a DOM, and happy-dom builds elements inside a textarea.<span><textarea></span><iframe srcdoc="<script>…">therefore rendered a live iframe running script in the page's origin. This affects 1.17.2 and earlier.sanitizeHtmlForSSR()drops HTML comments. Browsers end a comment early at<!-->,<!--->and--!>, so<!--><img src=x onerror=alert(1)>-->rendered a live element throughbq-htmlin SSR output.sanitizeHtmlForSSR()checks every URL attribute:action,formaction,xlink:href,poster, … and eachsrcsetcandidate, not onlyhrefandsrc.- The DOM backend of
sanitizeHtml()drops comments for the same reason as SSR. - The DOM backend drops non-text children of text-only elements (
textarea,title,xmp, …). DOM implementations such as happy-dom build such children, and a browser re-parsing the output ran the markup after an early</textarea>. Browsers never build them, so browser output is unchanged.
A new adversarial corpus of 129 payloads runs through all three sanitizer paths and is judged by an independent model of how a browser parses the output.
SSR: full-page templates
- Full-page templates keep a leading
<!DOCTYPE html>, so rendered pages no longer fall into quirks mode. - Templates with a doctype or a leading
<html>,<head>or<body>always use the DOM-free renderer. They keep their wrappers and head, asset and store-state injection, with or without a global DOM. renderToStream()streams a full page withflushBoundary()markers as one document, andrenderToStreamSuspense()sends the closing</body></html>after its patches.
View, store and testing
- View:
bq-onhandlers accept;-separated statements, as inbq-on:submit="$event.preventDefault(); add()". The compiler emits them in a separatestatementsmap, passed toregisterCompiledExpressions(expressions, statements?), so they stay CSP-safe. - Store:
createStore(),defineStore()andcreatePersistedStore()infer state, getter and action types from the definition. Getters and actions used to come out asneverwithout explicit generics. - Testing:
*ByRole()accepts{ name },find*queries take{ timeout, interval }and stop at the deadline, andfireEvent/userEventdispatch realMouseEvent,KeyboardEventand other typed events with the flags a browser uses.
Tooling
create-bquery:npx --package @bquery/bquery create-bquery my-appscaffolds a CDN page, a Vite SPA, an SSR app for Node, Bun or Deno, or a full-stack app.@bquery/bqueryships the scaffolder as itscreate-bquerybin, so it always matches the bQuery release it comes with. The standalonecreate-bquerypackage fornpm create bquery@latestis in development and not yet on npm. See Create a project.- Editor IntelliSense: the package ships
editor/html-custom-data.jsonforbq-*completion in VS Code, andeditor/bquery-config.schema.jsonfordefineBqueryConfig(). - ESLint plugin and bundler plugin (in development):
@bquery/eslint-pluginand@bquery/unpluginare developed in their own repositories, bQuery/eslint-plugin and bQuery/unplugin. They are not yet published on npm; their guides describe the planned first release. - CI: a Playwright lane runs a focused browser suite in Chromium, Firefox and WebKit, and a benchmark workflow fails a PR when a median regresses by more than 20 %.
Upgrading
- Component re-renders morph the DOM. Code that relied on fresh nodes after every render, for example comparing node identity or expecting user input to reset, can pass
renderStrategy: 'replace'. - Retries default to idempotent methods and transient statuses.
- To keep retrying
POST/PATCHon endpoints that are safe to repeat, passretry: { count, methods: ['POST', 'PATCH'] }(or['*']). - To keep retrying
500, passstatuses: [500, 502, 503, 504]. - A custom
retryOnstill replaces the default policy.
- To keep retrying
- Request bodies are limited by default. Routes that accept larger bodies need
createServer({ limits: { json: 5 * 1024 * 1024 } }), orInfinityto lift a limit. ctx.body()reads the request once. ARequesttaken fromctx.requestbeforectx.body()cannot be read afterwards; takectx.requestagain after the body was read, which is rebuilt from the cached bytes.- Tests that click submit buttons or checkboxes.
fireEvent.click()anduserEvent.click()dispatch real mouse events, so the DOM runs default actions: a submit button submits its form and a checkbox toggles. - Tests that listen for
focus,blurormouseenteron a parent.fireEventuses the browser's event flags, so these events no longer bubble. Listen forfocusin/focusout/mouseover, or pass{ bubbles: true }. - Store type annotations. A store without getters or actions is typed
Store<S, EmptyStoreRecord, EmptyStoreRecord>. Annotations written asStore<S, Record<string, never>, Record<string, never>>need the new type, exported from@bquery/bquery/store, orStore<any, any, any>.
Engines
Publish and local validation target Node.js ≥ 24.0.0 and Bun ≥ 1.4.0. See Supported Runtimes.