Instatic Tree Mutation API for Page and Visual Component Operations
The Instatic Tree mutation API exposes 11 pure mutation functions in src/core/page-tree/mutations.ts that modify a NodeTree<PageNode> structure through the central applyTreeOperation dispatcher, enabling consistent page and visual component tree manipulations across the visual editor and plugin VM.
Instatic represents every page and visual component (VC) hierarchy as a NodeTree<PageNode> living inside a SiteDocument. All structural changes—whether inserting nodes, moving subtrees, or updating props—flow through a strictly defined Instatic Tree mutation API centered in src/core/page-tree/mutations.ts. These functions are pure (operating on Zustand Mutative drafts) and exposed via a single entry point used by both the admin interface and the plugin runtime.
Core Data Structures
Understanding the mutation API requires familiarity with three foundational types defined across the core page-tree modules.
NodeTree and PageNode
The NodeTree<PageNode> (defined in src/core/page-tree/treeSchema.ts) is the mutable container holding a nodes map, a rootNodeId, and document metadata. Each entry in the map is a PageNode (src/core/page-tree/pageNode.ts) representing a module instance with properties for module ID, props, children IDs, breakpoint overrides, class IDs, and bookkeeping fields (parentId, label, locked, hidden).
TreeOperation Discriminated Union
All intended changes are described by a TreeOperation (src/core/page-tree/operationSchema.ts), a tagged union where each variant carries the minimal data required for a specific mutation. This strict typing ensures that the dispatcher can exhaustively handle every valid operation type without ambiguity.
The 11 Core Mutation Functions
The Instatic Tree mutation API provides 11 named primitives for page/VC operations. Each function mutates the supplied draft tree directly and is implemented in src/core/page-tree/mutations.ts.
createNode(moduleId, defaults?)— Factory returning a freshPageNodewith a unique ID and optional default props.insertNode(tree, node, parentId, index?)— Adds a node as a child of the specified parent at the given index; omitting the index appends to the end.deleteNode(tree, nodeId)— Removes a node and all its descendants from the tree, cleaning up the parent’s children array.updateNodeProps(tree, nodeId, patch)— Performs a shallow merge of new props into the existingnode.propsobject.setBreakpointOverride(tree, nodeId, breakpointId, patch)— Applies property overrides for a specific breakpoint.clearBreakpointOverride(tree, nodeId, breakpointId)— Removes breakpoint-specific overrides for the node.renameNode(tree, nodeId, label)— Updates the human-readablenode.labelfield.toggleNodeLocked(tree, nodeId)— Flips the booleannode.lockedflag.toggleNodeHidden(tree, nodeId)— Flips the booleannode.hiddenflag.moveNode(tree, nodeId, newParentId, newIndex)— Re-parents a node or reorders it within the same parent.duplicateNode(tree, nodeId, options?)— Deep-clones a subtree, assigns fresh IDs to all cloned nodes, and inserts the clone immediately after the source node.wrapNode(tree, nodeId, containerModuleId, containerDefaults?)— Inserts a new container node and makes the target node its first child.pasteSubtree(tree, payload, parentId, index?, options?)— Inserts a foreign subtree (e.g., from clipboard) after regenerating all IDs to avoid collisions.
Additional bulk helpers such as wrapNodes and moveNodes build upon these primitives using the same reduction logic employed by the UI layer.
The applyTreeOperation Dispatcher
All Instatic Tree mutation API calls converge on a single dispatcher: applyTreeOperation (src/core/page-tree/mutations.ts, lines 81–120). This function receives the tree draft and a TreeOperation object, switches on op.kind, and delegates to the appropriate mutation helper.
// src/core/page-tree/mutations.ts
export function applyTreeOperation(
tree: NodeTree<PageNode>,
op: TreeOperation,
): ApplyTreeOperationResult {
switch (op.kind) {
case 'insertNode':
insertNode(tree, op.node, op.parentId, op.index);
return { tree, affectedNodeIds: [op.parentId, op.node.id] };
case 'updateNodeProps':
updateNodeProps(tree, op.nodeId, op.props);
return { tree, affectedNodeIds: [op.nodeId] };
case 'setBreakpointOverride':
setBreakpointOverride(tree, op.nodeId, op.breakpoint, op.props);
return { tree, affectedNodeIds: [op.nodeId] };
case 'clearBreakpointOverride':
clearBreakpointOverride(tree, op.nodeId, op.breakpoint);
return { tree, affectedNodeIds: [op.nodeId] };
case 'renameNode':
renameNode(tree, op.nodeId, op.name);
return { tree, affectedNodeIds: [op.nodeId] };
case 'toggleNodeLocked':
toggleNodeLocked(tree, op.nodeId);
return { tree, affectedNodeIds: [op.nodeId] };
case 'toggleNodeHidden':
toggleNodeHidden(tree, op.nodeId);
return { tree, affectedNodeIds: [op.nodeId] };
case 'moveNode':
const oldParent = getParent(tree, op.nodeId);
moveNode(tree, op.nodeId, op.parentId, op.index);
return { tree, affectedNodeIds: oldParent ? [op.nodeId, op.parentId, oldParent.id] : [op.nodeId, op.parentId] };
case 'duplicateNode':
const newId = duplicateNode(tree, op.nodeId);
return { tree, affectedNodeIds: [op.nodeId, newId] };
case 'wrapNode':
const wrapperId = wrapNode(tree, op.nodeId, op.wrapper.moduleId, op.wrapper.defaults);
return { tree, affectedNodeIds: [op.nodeId, wrapperId] };
case 'deleteNode':
const parent = getParent(tree, op.nodeId);
deleteNode(tree, op.nodeId);
return { tree, affectedNodeIds: parent ? [parent.id] : [] };
}
}
The dispatcher never clones the tree; it mutates the supplied Zustand draft. Callers requiring an immutable snapshot must structuredClone the tree before invocation. Each case returns an ApplyTreeOperationResult containing the mutated tree and an array of affected node IDs for reactive UI updates.
Practical Implementation Examples
The following examples demonstrate common Instatic Tree mutation API workflows using the raw helpers and the dispatcher:
import {
createNode,
insertNode,
moveNode,
duplicateNode,
wrapNode,
applyTreeOperation,
} from '@core/page-tree';
// 1. Create a new button node and append it to the page root.
const btnNode = createNode('core/modules/button', { text: 'Click me' });
insertNode(pageTree, btnNode, pageTree.rootNodeId); // index omitted → append
// 2. Rename the button using the dispatcher.
applyTreeOperation(pageTree, {
kind: 'renameNode',
nodeId: btnNode.id,
name: 'Primary button',
});
// 3. Duplicate the button (inserts clone after source automatically).
const cloneId = duplicateNode(pageTree, btnNode.id);
// 4. Move the clone into a column container at index 0.
applyTreeOperation(pageTree, {
kind: 'moveNode',
nodeId: cloneId,
parentId: columnNodeId,
index: 0,
});
// 5. Wrap the original button in a new flex container.
const wrapperId = wrapNode(
pageTree,
btnNode.id,
'core/modules/flex',
{ direction: 'row' }
);
Integration Points: Editor and Plugin VM
The Instatic Tree mutation API serves as the single source of truth for tree consistency across two distinct consumers:
- Visual Editor: The Zustand store (
src/admin/pages/site/store/slices/site/helpers.ts) defines actions likeinsertNodeandmoveNodethat wrap these helpers inside amutateActiveTreecall, ensuring UI state stays synchronized with the draft mutations. - Plugin VM: Plugins executing in the QuickJS runtime invoke
applyTreeOperationvia the RPC endpointapi.cms.content.tree(...).mutate(seesrc/core/plugins/quickjs/bootstrap/src/...), allowing third-party code to perform structural changes safely.
Because both pathways funnel through the same applyTreeOperation dispatcher, all page-level and VC-level structural changes obey identical validation and side-effect logic.
Summary
- Instatic represents pages and visual components as a
NodeTree<PageNode>stored within aSiteDocument. - The Instatic Tree mutation API consists of 11 pure functions located in
src/core/page-tree/mutations.ts. - All mutations flow through the
applyTreeOperationdispatcher, which interpretsTreeOperationcommands fromoperationSchema.ts. - Functions like
insertNode,moveNode,duplicateNode, andwrapNodehandle structural changes, whileupdateNodePropsand breakpoint helpers manage data. - The API is consumed by both the Zustand-based editor store and the plugin VM, ensuring consistent behavior across the application.
Frequently Asked Questions
What is the difference between insertNode and pasteSubtree?
insertNode adds a single PageNode instance that you have already created (typically via createNode) into a specific parent at a specific index. pasteSubtree accepts a serialized foreign tree (e.g., from the clipboard), regenerates all node IDs to prevent collisions, and inserts the entire hierarchy. Use insertNode for programmatic creation and pasteSubtree for importing external or copied structures.
How does the mutation API maintain immutability?
The functions themselves are pure but operate on draft objects supplied by Zustand’s Mutative middleware. The tree passed into applyTreeOperation is already a mutable draft; the dispatcher and helpers mutate it directly. If you need an immutable result, you must structuredClone the tree before calling the API, as the functions do not perform defensive copying.
Can plugins directly import the mutation helpers?
No. Plugins run inside a QuickJS sandbox and do not have direct filesystem access to src/core/page-tree/mutations.ts. Instead, they invoke the api.cms.content.tree.mutate RPC method, which serializes a TreeOperation object and forwards it to the host environment where applyTreeOperation executes on the actual document state.
What happens to child nodes when deleteNode is called?
The deleteNode mutation performs a recursive removal. When you delete a node, the function removes that node’s entry from the tree’s nodes map and filters its ID from its parent’s children array. Because the tree stores nodes in a flat map with references to children IDs, deleting a parent effectively orphans its entire subtree, and subsequent operations should treat those IDs as invalid unless the nodes were explicitly moved beforehand.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →