Skip to content

@rezi-ui/node

Node/Bun backend package:

  • configurable engine execution mode (auto | worker | inline)
  • transfer of drawlists/events between core and native
  • buffer pooling and scheduling

Install

npm i @rezi-ui/node
# or
bun add @rezi-ui/node

What you get

  • A backend implementation that satisfies the @rezi-ui/core runtime backend interface
  • Worker and inline execution paths for the native engine
  • A stable message protocol for worker mode
  • Integration with @rezi-ui/native (prebuilt binaries when available)

Execution mode

Set config.executionMode on createNodeApp(...):

  • auto (default): inline when fpsCap <= 30; otherwise prefer worker and fall back to inline when no TTY or nativeShimModule is available
  • worker: always run the engine on a worker thread
  • inline: run the engine inline on the main JS thread
import { createNodeApp } from "@rezi-ui/node";

const app = createNodeApp({
  initialState: { count: 0 },
  config: {
    executionMode: "auto",
    fpsCap: 60,
    maxEventBytes: 1 << 20,
  },
});

createNodeApp is the default path because it keeps core/backend config in lockstep:

  • app/backend maxEventBytes
  • app/backend fpsCap

Hot State-Preserving Reload (HSR)

createNodeApp(...) has first-class HSR wiring through hotReload.

import { ui } from "@rezi-ui/core";
import { createNodeApp } from "@rezi-ui/node";

const app = createNodeApp({
  initialState: { count: 0 },
  hotReload: {
    viewModule: new URL("./screens/main-screen.ts", import.meta.url),
    moduleRoot: new URL("./src", import.meta.url),
  },
});

app.view((state) => ui.text(`count=${String(state.count)}`));
await app.run(); // starts/stops hot reload watcher with app lifecycle

Route-managed apps use routesModule:

const app = createNodeApp({
  initialState: { count: 0 },
  routes,
  initialRoute: "home",
  hotReload: {
    routesModule: new URL("./screens/index.ts", import.meta.url),
    moduleRoot: new URL("./src", import.meta.url),
    resolveRoutes: (moduleNs) => {
      const routesExport = (moduleNs as { routes?: unknown }).routes;
      if (!Array.isArray(routesExport)) {
        throw new Error("Expected `routes` array export");
      }
      return routesExport;
    },
  },
});

app.hotReload exposes the controller (reloadNow(), isRunning()) when you need manual trigger points. For advanced/custom lifecycle control, low-level createHotStateReload(...) remains available.

What this does:

  • watches source paths for changes
  • re-imports the target module from a fresh module snapshot
  • calls either app.replaceView(...) or app.replaceRoutes(...) without restarting the process

What stays intact across reload:

  • app state (app.update)
  • focused widget (when the same id still exists)
  • local widget hook state (defineWidget) when keys/ids remain stable

Current scope:

  • widget-mode apps (app.view/app.replaceView)
  • route-managed apps (createNodeApp({ routes, initialRoute }) + app.replaceRoutes)
  • not raw draw mode

NO_COLOR behavior

createNodeApp(...) checks process.env.NO_COLOR at app construction time. When present, Rezi forces a monochrome theme and exposes:

const app = createNodeApp({ initialState: {} });
app.isNoColor; // boolean

This supports CI and accessibility tooling that relies on the no-color.org convention.

Backend access (advanced)

Most apps should use createNodeApp() so app/core and backend settings stay aligned automatically.

If you need direct backend access (benchmarks/custom runners), you can either:

  • read it off createNodeApp(...) via app.backend, or
  • construct it directly with createNodeBackend() and pass it to createApp() from @rezi-ui/core.

Native engine config passthrough

createNodeApp({ config: { nativeConfig } }) forwards nativeConfig to the native layer’s engine creation config.

Keys are forwarded as-is. If you want a close match to the engine’s public C structs, use snake_case field names:

import { createNodeApp } from "@rezi-ui/node";

const app = createNodeApp({
  initialState: { count: 0 },
  config: {
    fpsCap: 60,
    nativeConfig: {
      target_fps: 60, // must match fpsCap when provided
      limits: {
        dl_max_total_bytes: 16 << 20,
      },
    },
  },
});

See: