@brust/runtime-dom

中文文档
2.0.0-alpha.38发布于 2026/11/05TypeScriptES Module零依赖近 30 天下载 6747
原始使用文档
文档目录8 节
  1. Public surface
  2. Directive value grammar
  3. Attributes
  4. First-paint rule
  5. Gates
  6. React islands
  7. Store
  8. Feature modules (L33, F139)

@brust/runtime-dom

React-free browser runtime that binds a compiled client chunk (defineBehavior) to server-rendered HTML through x-* attributes. This README is the output format the M1c client backend prints against; every behaviour below is pinned by a test in test/. Zero dependencies, eval-free, bundle < 18 KB minified (bun scripts/build.ts).

Public surface

import { signal, computed, effect, batch, untracked, defineBehavior, setChunkLoader, mount, unmount } from '@brust/runtime-dom'
  • Signals (test/signal.test.ts): writes are Object.is-deduped; computed is lazy and cached; effect(fn) runs now and on change, a returned function is a cleanup that runs before each re-run and on dispose; batch defers effects to its end; untracked reads without subscribing.
  • defineBehavior(name, factory): factory({ el, props, effect, onCleanup, ref }) returns the instance members (signals, computeds, functions, plain values). props is a signal of the props object. An init member, if a function, runs after the factory.
  • setChunkLoader(load): called once per unknown behavior name; the loader is expected to call defineBehavior(name, …). Default: none (hosts wait).
  • mount(root = document.body): idempotent; mounts every [x-data] in document order (parents first) and starts a MutationObserver that mounts added hosts and disposes removed ones (a removed-then-reinserted host is disposed, then mounted fresh). unmount(root) disposes instances under root and stops the observer. A factory that throws is logged and its instance discarded; other hosts still mount.

Directive value grammar

value    := path (":" bindings)?
path     := ident ("." ident)*          // looked up on the instance members; a signal/computed hop is unwrapped
bindings := ident ("," ident)*          // loop-scope names (x-for item/index); the member must be a function and is called with them

Anything else (x + 1, fn()) is not parsed: a warning is logged once and the DOM is left as rendered. An unknown member behaves the same.

Attributes

Directives come only from compiler output: the compiler never prints an x-* attribute written in component JSX (warning reserved-attr), and the runtime does not bind inside dangerouslySetInnerHTML markup (x-html / x-raw) or a React island (<brust-island>).

  • x-data="name": one instance per host. Nested hosts are bound by their own instance, not the parent's walker.
  • x-props='{"json":1}': seeds ctx.props. Invalid JSON warns and seeds {}.
  • x-props-bind="member[:binding]": on a child host, rebinds the child's props signal reactively to the nearest ancestor instance's member (an object, typically a computed; function values pass through). Inside x-for the row's scope supplies the bindings. If the parent is not mounted yet (chunk still loading) the child links up when the parent mounts. No ancestor: warns, keeps the x-props seed.
  • x-text="value": sets textContent; null/undefined → ''.
  • x-html="value": sets innerHTML (raw markup, unescaped); null/undefined → ''. Emitted only by the compiler, for a dangerouslySetInnerHTML whose value changes after first paint; the author sanitises the markup (same contract as React). The first-paint check compares parsed markup, so equivalent spellings do not warn.
  • x-show="value": style.display = '' when truthy, 'none' otherwise.
  • x-if="value": the element is replaced by a <!--x-if--> comment anchor; a clone is inserted after it while truthy and removed otherwise. Nested x-data hosts in the clone mount/dispose with it. Server HTML omits the element when false.
  • x-bind-<attr>="value": class → className; value → .value; disabled checked selected readonly required hidden open multiple → property + attribute (present/absent); anything else: null/undefined/false removes the attribute, otherwise setAttribute. Refused (warn once): on* and srcdoc attributes, and, in href src action formaction poster data, any URL whose parsed scheme is not http(s):/mailto:/tel: (relative URLs are fine; java\tscript: is caught because the URL is parsed, not pattern-matched).
  • x-on-<event>="member[:binding]": calls the member with the bound scope values first, then the event: pick(item, index, event).
  • x-model="member": the member must be a writable signal (else warns). text-like: input event ↔ .value; checkbox: change ↔ boolean .checked; radio: change writes .value when checked, signal sets .checked; single select: change ↔ .value, re-applied whenever its options change. select[multiple] is unsupported (warns).
  • x-for="item[, index] in source by keyFn": source := path (":" bindings)? is a member path to an array (or signal/computed of one); with bindings the member is a function called with the outer loop names, so a nested list reads the outer row, e.g. <tr x-for="row in rows by k1"><td x-for="c in cells:row by k2">; keyFn is a member function of the item. Server-rendered sibling rows carrying the same x-for are adopted in order on first run; with zero rows the server emits a <!--x-for--> comment followed by one hidden template element (carrying x-for) which is cloned. Rows are reconciled by key (moves keep DOM identity); a reused row whose item/index changed has its binders re-run. Duplicate keys warn once and fall back to index identity. Elements with x-for are owned by the directive: their other x-* attributes are bound on the rows, with scope { item, index }.
  • x-ref="name": ctx.ref('name').current is the element after mount, null after dispose.

First-paint rule

Nothing re-renders on mount: server HTML is the initial state. A binder writes the DOM only when its effect re-runs after a change, or when the initial x-text / x-html value differs from the server text, in which case it corrects it and warns once (first-paint mismatch …): a mismatch is a compiler bug the runtime must not hide. Elements created after mount (new rows, x-if clones) never trip this check.

Gates

bun test · bunx tsc --noEmit -p . (bun check needs Bun ≥ 1.4.3) · bun scripts/build.ts (fails if the bundle imports React).

React islands

The server paints a react-tier component as <brust-island data-id="<componentId>" x-props='<json>'>…server HTML…</brust-island>. The island's chunk registers its hydrate function without importing the runtime:

;(globalThis.__brustIslands ||= []).push(['<componentId>', (host, props) => hydrateRoot(host, <Comp {...props}/>)])
globalThis.__brustIslandReady?.()

The runtime hydrates every host once, when the browser is idle (requestIdleCallback, else setTimeout(…, 1)), and sets data-hydrated="1" on success. Load order between the runtime script and island chunks does not matter. Bad x-props JSON hydrates with {} and warns once; a hydrate that throws is reported on console.error and the server HTML stays. This is the only strategy in M2 (spec S12).

Store

The 0.1.x brustjs/store API on the same reactive core the directives use (one graph, even across module copies: tracking state lives on globalThis[Symbol.for('brust.reactive.ctx')]).

  • @brust/runtime-dom/store (also the runtime entry, and @brust/core/store): signal computed effect batch untracked isSignal isComputed defineStore and the types Signal Computed Dispose StoreHandle Snapshot.
  • defineStore(name, factory) returns a handle: store.field, store.snapshot(), store.serialize(), store.hydrate(state), store.subscribe(cb). First factory wins per name.
  • The browser keeps one instance per name in globalThis[Symbol.for('brust.stores')]. The first access to a name seeds it from <script type="application/json" data-brust-store="NAME"> (lazy, once).
  • @brust/runtime-dom/store/react (also @brust/core/store/react): useStore(store) via useSyncExternalStore. React is an optional peer, imported only by this subpath; the runtime bundle stays React-free.
  • @brust/runtime-dom/store/internal: the server-resolver hook and serialization helpers the Bun worker uses. Not part of the public surface.

Feature modules (L33, F139)

The runtime is a core plus feature modules, so a page loads only what its components use and the client JS does not grow as the compiler learns more of React.

  • @brust/runtime-dom/core (src/core.ts): signals, defineBehavior/setChunkLoader, mount/unmount, the binder registry (BINDERS) and the internals features need (src/core-api.ts). It knows no directive by name.
  • @brust/runtime-dom/features/<name> (src/features/*.ts): basics (x-text, x-show, x-bind-*, x-on-*, x-ref), if, for, model, html, island (React island hydration glue). A feature registers itself with the core when imported and imports the core only through core-api; in the browser build core-api is the shared core file, so there is one signal graph and one binder registry per page.
  • @brust/runtime-dom (src/index.ts) is core + every feature, for tests and non-split consumers. The browser build never uses it.

The rule for a new feature. The micro-directives nearly every interactive page uses ship together in basics. Anything else gets its OWN module when it is at least about 1 KB gzip OR used by a minority of pages: store, island, if/for/model/html as measured, and every future feature (context, portal, focus trap, ...). Do not add a feature to basics or the core to save a file; add it as a module the compiler requests only for routes that use it. The CI byte budget (core + basics) holds the line.

版本与发布记录

@brust/runtime-dom 共发布 39 个版本,首次发布于 2026/10/12;过去 12 个月发布了 39 个版本。

版本发布日期距今
2.0.0-alpha.38latest2026/11/053 天前
2.0.0-alpha.372026/11/053 天前
2.0.0-alpha.362026/11/044 天前
2.0.0-alpha.352026/11/044 天前
2.0.0-alpha.342026/11/045 天前
2.0.0-alpha.332026/10/2910 天前

关于 @brust/runtime-dom 的常见问题

如何安装 @brust/runtime-dom?

在项目目录运行 npm install @brust/runtime-dom,或使用 pnpm add / yarn add / bun add。

@brust/runtime-dom 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@brust/runtime-dom 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@brust/runtime-dom 可以商用吗?

@brust/runtime-dom 使用 未声明 许可。package.json 没有声明许可证。没有许可证的代码默认保留全部权利,商用前请联系作者或查看仓库中的 LICENSE 文件。以上为概括,不构成法律意见,请以许可原文为准。

@brust/runtime-dom 的下载量有多少?

根据 npm 官方下载统计,@brust/runtime-dom 在 2026/10/09 – 2026/11/07 这 30 天内被下载 6,747 次,日均约 225 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@brust/runtime-dom 还在维护吗?

最新版本 2.0.0-alpha.38 发布于 2026/11/05(3 天前);至今共发布 39 个版本,其中过去 12 个月发布了 39 个;首次发布于 2026/10/12。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。