Runtime & Core API

The nuforge runtime lives in @nuforge/core. The central object is Nu — it owns the document, drives reactivity, and exposes the transactional, undoable API you use to mutate and observe a page. This page walks through the lifecycle, mutations and history, change listeners, node lookup, and the remaining members of the runtime.

Lifecycle

You create a runtime with Nu.create(), load a document into it, and then create frames. A frame renders one component instance, and frame.view is the reactive View tree the host renders.

import { Nu } from '@nuforge/core';
import { Parser } from '@nuforge/parser';
import * as t from '@nuforge/types';

const nu = Nu.create();
nu.load(t.state({ program: Parser.parseProgram(SOURCE) }));

const frame = nu.createFrame({ component: { name: 'App' } });
frame.view; // the reactive View tree

A frame renders one component instance, and frame.view is reactive — read it inside a reactive context and it re-runs whenever the underlying state changes. You can look frames up again with nu.getFrame(id) and tear them down with nu.removeFrame(frame).

Mutations & history

Every change goes through nu.change. A change runs as a single transaction: the writes inside the callback are applied together and recorded as one undoable step.

const app = nu.state.program.components[0];
nu.change(() => {
  app.template.props.title = t.literal({ value: 'Hello' });
});

nu.undo();
nu.redo();
nu.canUndo();
nu.canRedo();

History is granular: the runtime records just the field-level writes a change made, not a full snapshot of the document. That makes undo and redo cheap in both time and memory — undoing a change replays the inverse of exactly those writes, so cost scales with how much you touched, not with how large the document is. Use nu.canUndo() and nu.canRedo() to gate UI affordances.

Listening to changes

There are two complementary subscriptions. nu.listenToChangeset gives you a structural diff per change — which nodes were added or disposed — while nu.listenToMutations gives you the individual field-level writes.

nu.listenToChangeset((changeset) => {
  // { added, disposed, source } — structural diff per change
});

nu.listenToMutations((entries) => {
  // field-level writes — { target, key, before, after, ... }
});

Use the changeset stream to react to structural edits (a node appeared or was removed), and the mutation stream when you need the precise before/after of each written field.

Looking up nodes

The runtime indexes every node by id, so you can resolve nodes, walk to parents, and find source locations.

nu.getNodeFromId(id);
nu.getParentNode(node);
nu.getNodeLocation(node);

getNodeFromId resolves an id to its live node, getParentNode returns the node's parent in the tree, and getNodeLocation returns where the node sits in the source.

Other members

A few more members round out the runtime:

  • nu.program — the loaded program.
  • nu.resolver — resolves names and bindings.
  • nu.externals — host-provided values and functions (see Externals).
  • nu.getExtension(definition) — access a registered extension (see Extensions).
  • nu.volatile — off-document, non-serialized state.
  • nu.evaluateExpression(...) — evaluate an expression against runtime state.
  • nu.watch(cb) — run a callback reactively.
  • nu.toJSON() — serialize the current document.
  • nu.dispose() — tear down the runtime.

Nu.create(opts?) accepts an options object of the shape { externals, extensions } to register host externals and extensions up front.

Next steps

  • Externals — provide host values and functions to the runtime.
  • Extensions — attach plug-in state to a Nu runtime.
  • React — render frames in a React host.