@brust/core

中文文档
2.0.0-alpha.38发布于 2026/11/05MITES Module近 30 天下载 5525

brust v2 — React components compiled to Rust-rendered templates

原始使用文档
文档目录5 节
  1. Routes ( routes.tsx )
  2. brust build [entry=routes.tsx] [--out-dir dist]
  3. brust dev [entry=routes.tsx] [--port N] [--workers N]
  4. brust start [--port N] [--workers N] [--dist-dir dist] [--e…
  5. Worker wire (contract 7, crates/brust-server/src/protocol.r…

@brust/core

Rust serves the pages; N Bun workers answer only loader and jobs calls. One napi addon (crates/brust-napi, loader generated into native/ by napi build) binds brust-server and brust-compiler. Workspace-private (0.0.0) until m2e publishes it; peers react/react-dom 19.

Routes (routes.tsx)

import { defineRoutes, notFound } from '@brust/core/routes'
export const routes = defineRoutes([
  { Component: AppLayout, children: [                     // layout renders <Outlet />
    { path: '/', Component: HomePage },                   // static: never calls Bun
    { path: '/items/{id}', Component: ItemPage,
      loader: async ({ params }) => (params.id === 'x' ? notFound({}) : { item: await load(params.id) }),
      cache: { ttl_seconds: 60, tags: ['items'] } },      // a HIT never calls Bun
  ] },
])

Route fields are exactly path, Component, loader, cache, ssg, children (ssg only matters to --target static, see docs/static.md); anything else fails the build with <field> is not supported in M2 (M3). Loaders run root → leaf, results merge flat (later keys win); return notFound(data) / redirect(location, status) or throw httpError(status, body) for a verdict. cache(Comp, opts) and cache.invalidate(…) ship too.

brust build [entry=routes.tsx] [--out-dir dist]

--target static [--strict] also renders every page to dist/static/ for Cloudflare Pages or Workers Static Assets: see docs/static.md.

Compiles every route component through lowering, then writes (always from scratch):

dist/manifest.json        the only build → server contract (spec S6)
dist/jinja/<id>.jinja     templates          dist/jobs.js + jobs/*.server.ts   worker jobs
dist/client/*-<hex>.js    runtime, native chunks, react-<id> island chunks (served immutable)
dist/client/*-<hex>.css   one minified stylesheet per imported .css / .module.css (see docs/css.md)
dist/public/              copied from ./public          dist/index.js   `bun dist/index.js` = start

Every route component's children[] also gets one static entry per native descendant (at any depth) that has a client chunk, so its <script> is linked once per page.

A diagnostic (nested-instance, outlet-in-react, outlet-outside-layout, unsupported route field, unresolvable Component) prints error <rule> <message> and exits 1. The route tree is validated whether or not the entry used defineRoutes (route-config, duplicate-route for two patterns the router cannot hold together, e.g. /a and /a/).

Server-only code never reaches a browser bundle (error server-only-in-client <importer> imports <spec>, at any import depth, react islands included): node:* / bun:* / bare Node builtins (unless an npm package of that name is installed), @brust/core/server|native, any *.server.* file, and the paths/prefixes listed in brust.toml:

[build]
server_only = ["lib/server", "@acme/db"]   # import prefixes, or paths relative to the app root

The server-only marker package is honoured the same way: a browser build that reaches import 'server-only' (any depth, islands included) fails with server-only-in-client whether or not the package throws, and on the server side (route scan, jobs, actions, the Worker) it resolves to an empty module, so a loader may import a module guarded by it.

Loaders and precompute jobs may use all of it (they run in Bun). In the browser @brust/core resolves to a side-effect-free entry (cache, route helpers, verdicts; cache.invalidate throws).

The out dir is rebuilt in a sibling temp dir and swapped in only on success (a failed build leaves the previous one untouched); an out dir that is the filesystem root, the home dir, the app root or an ancestor of it, or that holds the entry / a route Component / public/, is refused (out-dir-unsafe). Relative imports of generated files resolve against the component's source dir, bare imports against the app root, so --out-dir may point anywhere; at run time dist/ must still resolve react and @brust/core (keep it inside the app, or next to a node_modules that has them).

brust dev [entry=routes.tsx] [--port N] [--workers N]

The Bun-target dev server. A long-lived parent owns your port and proxies to the newest build; every successful build is booted as a new child (brust start on a loopback port, --workers 1 unless you pass --workers), the proxy switches over, browsers are told to reload, and the old child is stopped. A failed build or a failed boot keeps the old child serving and shows the error as an overlay above the stale page (a page opened while builds fail shows it too); the next good build clears it and reloads.

  • Source changes do a full reload (debounced ~150 ms; an edit made during a rebuild is not dropped).
  • A change to public/*.css skips the build: the file is copied into the running dist and the <link> is swapped (?v=), the page keeps its state. Other public/ files reload.
  • Every text/html response, including error pages, gets /_brust/dev/client.js (a WebSocket at /_brust/dev/ws); responses are cache-control: no-store. Host and Origin reach the child unchanged, so the actions cross-site guard behaves as in production.
  • Dev builds use React in development mode (NODE_ENV=development); brust build / brust start are unchanged.
  • The terminal shows request lines (method path status ms cache) and a status block of at most 6 lines at the bottom (state, URL, generation, build time, last request, warnings, hotkeys r reload, o open, w warnings, c clear, q quit). It never uses the alternate screen. Not a TTY, CI or NO_COLOR: plain lines only.
  • Trace for dev tools (L14). The dev proxy generates a token per session and gives it to each child (BRUST_DEV_TOKEN, napi devToken) and to every upstream request (x-brust-dev-token). Only then the core answers x-brust-dev: base64(json) (route, pattern, chain, params, cache outcome, loader and render ms, jobs with kind/key/tags/hit, bun calls, the components the page is made of). It is added after the L1 store, so it is never cached; a core without a token (production) never makes it. The proxy strips the header, puts the trace in the page as <script type=application/json id=__brust_trace> and keeps the last 50 (actions included) at /_brust/dev/traces. brust build --dev (used by brust dev) also writes dev.json (component id → source file, tier, the react reason, diagnostics with rule/line/col/remediation; served at /_brust/dev/meta); a production build writes none. In dev only, every chain level is wrapped in <!--brust:<id>-->…<!--/brust--> (never inside a <brust-island>), and the runtime exposes its live instances on globalThis.__brustDev.instances (__BRUST_DEV__ is a build define that is false, and removed by the minifier, in every other build).
  • <brust-devtools> (L15). The dev proxy serves /_brust/dev/devtools.js (bundled from src/dev/devtools.ts on first request) and injects it into every HTML response, so brust start and production builds contain none of it. A corner indicator shows the build state and the page's tier counts (ready · 2s 1n 1r); Alt+Shift+B toggles the panel (real buttons and tabs, arrow keys, Esc closes and restores focus, contrast ≥ 4.5:1 in light and dark). Panels, each from the trace, dev.json, the DOM or __brustDev only: Page (route, params, cache, timings), Components (tree with tier badges; a react component shows its reason and diagnostic location; hover highlights it), Jobs, Store (seed from data-brust-store, live values of mounted native stores), Islands (hydrated or not), Build (diagnostics with file:line:col, the current build error), Requests (the last 50). The terminal status block shows the same tier counts for the last page.
  • Not hot module replacement: component state does not survive an edit. brust dev --target cloudflare keeps its wrangler loop (now on the same watch loop).

brust start [--port N] [--workers N] [--dist-dir dist] [--entry routes.tsx]

Prints [brust] listening on <addr> then [brust] ready (N workers). --port 0 picks a free port. SIGINT/SIGTERM drains gracefully (BRUST_DRAIN_TIMEOUT_MS); a second signal exits at once. Config precedence: env (BRUST_ADDR, BRUST_PORT, BRUST_WORKERS, BRUST_RENDER_SLOTS, BRUST_DRAIN_TIMEOUT_MS, BRUST_BOOT_TIMEOUT_MS, BRUST_CALL_TIMEOUT_MS, BRUST_SLOT_BYTES, BRUST_MAX_LOADER_RESPONSE_BYTES, BRUST_DIST_DIR, BRUST_APP_ENTRY) > flags (--port, --workers, --dist-dir, --entry) and run({ config }) > brust.toml ([server] address/port/max_loader_response_bytes, [workers] count/slot_bytes, [cors]) > defaults (localhost:1337, one worker per CPU, min(cores, 16) slots per worker (each slot is a slotBytes shared response buffer, 256 KiB by default, so 10 slots = 2.5 MiB per worker), 10 s drain, 30 s worker boot timeout, 30 s call timeout, 8 MiB loader response limit). slotBytes ([workers] slot_bytes, env BRUST_SLOT_BYTES, run({ config: { slotBytes } })) is 64 KiB..16 MiB. A loader, jobs or planned reply that does not fit its slot is not an error: the worker hands it to the server over the heap, up to maxLoaderResponseBytes ([server] max_loader_response_bytes, env BRUST_MAX_LOADER_RESPONSE_BYTES, run({ config: { maxLoaderResponseBytes } }); 256 KiB..64 MiB, default 8 MiB). A bigger reply answers {"error":"response too large: <n> > <limit> (maxLoaderResponseBytes)"} (a 500). This limit applies to loader and jobs replies; [actions] has its own max_response_bytes. [cors] can also be given as run({ config: { cors: { origins, methods, headers, exposeHeaders, credentials, maxAgeSeconds } } }) (camelCase, an unknown key is rejected). It replaces a brust.toml [cors] as a whole, never merging with it; cors: null turns the toml table off. A loader or job call that has not settled within BRUST_CALL_TIMEOUT_MS answers 504; its worker slot stays claimed until the call settles (the late result is discarded), so a stuck call costs one slot, never a frozen worker. Waiting for a free slot is the separate 503 "all workers busy". GET /_brust/cache/stats reports L1/job cache counters plus loader_calls / job_calls / timed_out_calls. It is off by default (404): set [server] stats to "public" or "token" (stats_token = "env:NAME") to read it; brust dev serves it.

Worker wire (contract 7, crates/brust-server/src/protocol.rs)

Requests arrive as inline JSON; responses go into the worker's SharedArrayBuffer slot (never past floor(len / slots) bytes). A loader/jobs reply that does not fit crosses by heap: the worker puts the bytes with putFetchReply and writes the 5-byte marker [0x00][u32 LE n] into the slot (JSON never starts with 0x00); the server reads the n bytes it was handed. A loader context too large for the slot is planned from the bytes (planJobsBuf). Over maxLoaderResponseBytes the reply becomes {"error":…}. A handler never rejects.

loader {routeId, params, path, req} → {ok:true, data} | {verdict, …} | {error}; jobs {jobs:[{id, componentId, kind, inputs, target?, row?}]} → {results:[{id, value}|{id, error}]} (an ssr job renders jobs[target] with the manifest job's literals merged over inputs).

M2 limits: the route fields above only; per-row child instances one level deep (a nested instance is a build error); a react child's props must be props paths or literals; no worker respawn. Gates: bun run build:debug && bun run typecheck && bun test (CI runs each server-starting file alone; e2e: bun test --timeout 120000 test/e2e.test.ts).

版本与发布记录

@brust/core 共发布 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/core 的常见问题

@brust/core 是什么?

brust v2 — React components compiled to Rust-rendered templates

如何安装 @brust/core?

在项目目录运行 npm install @brust/core,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(^19.0.0)、react-dom(^19.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。它还提供命令行工具 brust,可以用 npx brust 直接运行。

@brust/core 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/brust__core 是否存在。

@brust/core 支持 ESM 还是 CommonJS?

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

@brust/core 可以商用吗?

@brust/core 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@brust/core 的下载量有多少?

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

@brust/core 还在维护吗?

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