# Instatic's Tree Mutation API: A Tree-Agnostic Architecture for Document Mutations

> Explore Instatic's tree mutation API for tree-agnostic document manipulation. Apply mutations to any NodeTree structure identically for pages, components, and layouts without branching. Read more.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: internals
- Published: 2026-07-28

---

**Instatic's tree mutation API provides pure, mutative-compatible functions in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts) that operate on any `NodeTree<PageNode>` structure, enabling identical manipulation of pages, visual components, and layouts without document-type branching.**

Instatic represents every hierarchical document—from pages to visual components—as a **NodeTree** of `PageNode` objects. Because a *Page* itself is simply a `NodeTree<PageNode>` containing `nodes` and `rootNodeId`, the same mutation logic applies universally across all tree-like structures in the CoreBunch/Instatic repository. This design eliminates redundant code paths while ensuring consistent behavior between the visual editor and plugin ecosystems.

## What Makes Instatic's Tree Mutation API Tree-Agnostic?

The tree-agnostic nature stems from generic type signatures and unified implementation patterns that avoid discriminating between document types.

### Generic NodeTree Interface

Every mutation function receives a `NodeTree<PageNode>` as its first argument. Since the tree shape is defined solely by the generic `NodeTree<TNode>` interface, the API treats page trees, visual-component trees, and layout hierarchies identically. This generic approach means the same `insertNode` or `moveNode` call works without modification regardless of the specific document being edited.

### Pure Mutative Design Pattern

The functions mutate the supplied draft directly while remaining safe for deep-cloned objects via `structuredClone`. This pure mutative compatibility allows the visual editor—via Zustand's Mutative middleware—and external plugins—via VM boundaries—to share the exact same implementation without side effects or state synchronization issues.

### Absence of Type-Specific Branching

The implementation satisfies the `no-vc-mode-branches-in-mutations` rule by containing no `kind === 'visualComponent'` conditionals. The same set of eleven mutation functions handles all tree operations uniformly, whether manipulating a root page or a nested visual component.

## Core Mutation Functions and Usage

Located in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts), the API exports eleven primary mutation functions: `createNode`, `insertNode`, `deleteNode`, `updateNodeProps`, `setBreakpointOverride`, `clearBreakpointOverride`, `renameNode`, `toggleNodeLocked`, `toggleNodeHidden`, `moveNode`, `duplicateNode`, and `wrapNode`.

### Creating and Inserting Nodes

```typescript
import { createNode, insertNode } from '@core/page-tree';

const draft: NodeTree<PageNode> = {
  rootNodeId: 'root',
  nodes: {
    root: { 
      id: 'root', 
      moduleId: 'root-module', 
      children: [], 
      parentId: null, 
      props: {}, 
      breakpointOverrides: {}, 
      classIds: [] 
    },
  },
};

const newNode = createNode('text-module', { text: 'Hello, world!' });
insertNode(draft, newNode, 'root');

```

### Moving Nodes Within the Tree

```typescript
import { moveNode } from '@core/page-tree';

// Moves 'node-id-to-move' to index 2 under 'new-parent-id'
moveNode(draft, 'node-id-to-move', 'new-parent-id', 2);

```

## The Unified Operation Dispatcher

The `applyTreeOperation` function serves as a thin wrapper that maps discriminated-union `TreeOperation` types to the concrete mutation functions. Both the visual editor and plugin SDK call this dispatcher, ensuring a single source of truth for tree transformations.

```typescript
import { applyTreeOperation } from '@core/page-tree';
import type { TreeOperation } from '@core/page-tree/operationSchema';

const op: TreeOperation = {
  kind: 'renameNode',
  nodeId: 'some-node',
  name: 'New Label',
};

const result = applyTreeOperation(draft, op);
// result.affectedNodeIds lists IDs requiring cache invalidation

```

## Supporting Infrastructure

Several modules collaborate to support the tree mutation API:

- **[`src/core/page-tree/selectors.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/selectors.ts)**: Provides traversal utilities including `getParent`, `isAncestor`, and `collectSubtreeIds` used by mutation functions to navigate the tree structure.
- **[`src/core/page-tree/operationSchema.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/operationSchema.ts)**: Defines the `TreeOperation` discriminated union that the `applyTreeOperation` dispatcher consumes.
- **[`src/__tests__/page-tree/mutations.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/page-tree/mutations.test.ts)**: Validates tree-agnostic behavior across all mutation scenarios.

## Summary

- **Instatic's tree mutation API** operates on the generic `NodeTree<PageNode>` interface in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts), treating pages, visual components, and layouts identically.
- **Pure mutative functions** work with both Zustand's Mutative middleware and plugin VM boundaries without code duplication or side effects.
- **Eleven core functions** handle node lifecycle operations without document-type branching, adhering to the `no-vc-mode-branches-in-mutations` rule.
- **`applyTreeOperation`** provides a unified dispatcher consumed by both the visual editor and plugin SDK, returning affected node IDs for cache invalidation.

## Frequently Asked Questions

### How does Instatic's tree mutation API handle different document types without branching logic?

The API treats all hierarchical structures as generic `NodeTree<PageNode>` instances. Because pages, visual components, and layouts share the same tree shape—containing `nodes` and `rootNodeId`—the mutation functions require no `kind === 'visualComponent'` checks or type-specific conditionals, satisfying the `no-vc-mode-branches-in-mutations` architectural rule.

### What makes the mutation functions "pure" yet "mutative"?

Each function mutates the supplied draft object directly (mutative behavior), but because they perform no external side effects and work safely with `structuredClone` outputs, they remain pure from a functional programming perspective. This allows seamless integration with both immutable state libraries like Zustand and direct mutation scenarios across VM boundaries.

### Can plugins use the same mutation API as the visual editor?

Yes. The `applyTreeOperation` dispatcher in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts) provides a shared entry point that both the visual editor and plugin SDK consume. This guarantees identical mutation semantics regardless of which environment initiates the tree operation, with the dispatcher returning `affectedNodeIds` for proper cache invalidation.

### Where is the tree-agnostic behavior tested?

The test suite in [`src/__tests__/page-tree/mutations.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/page-tree/mutations.test.ts) validates that all eleven mutation functions behave correctly across different tree configurations, confirming the API's agnostic design through comprehensive unit tests against the `NodeTree<PageNode>` interface.