sitesmith: pre-create section-inner/bg-section-inner/form-inner linkedNodes

The Invariant 'component type (undefined) does not exist in the resolver'
was Craft.js's toNodeTree choking on the linkedNode that <Element id="X">
auto-creates at render time inside Section / BackgroundSection /
FormContainer. The auto-created node stores its type as the Container
React component class itself, not as {resolvedName:'Container'}, so the
later type.resolvedName lookup returns undefined.

For each shell, treeToState (and apply-ai-response's buildNodeTree) now
synthesizes the linkedNode container up-front with a proper serialized
type, moves the AI's direct children into it, and reparents them. This
matches the canonical shape Craft.js writes when the user manually builds
a site, so Craft.js never has to materialize the linkedNode itself.
This commit is contained in:
2026-05-24 17:22:40 -07:00
parent a1ec51afc3
commit 87dd4340f7
2 changed files with 94 additions and 0 deletions

View File

@@ -10,6 +10,12 @@ import { SitesmithResponse, SerializedTreeNode } from '../types/sitesmith';
* canvas but its render ignores `data.nodes`. */
const CANVAS_TYPES = new Set(['Container']);
const SHELL_INNER: Record<string, string> = {
Section: 'section-inner',
BackgroundSection: 'bg-section-inner',
FormContainer: 'form-inner',
};
/**
* Flatten a SerializedTreeNode tree into a Craft.js node map ready for
* `actions.deserialize()`.
@@ -100,6 +106,35 @@ function buildNodeTree(query: any, tree: SerializedTreeNode): NodeTree {
craftNodes[id].data.props.columns = colCount;
}
}
// Section/BackgroundSection/FormContainer wrap their content in a single
// <Element id="<key>" is={Container} canvas>. Pre-create the linkedNode
// so Craft.js doesn't auto-create one with a malformed type field.
const innerKey = SHELL_INNER[node.type.resolvedName];
if (innerKey && craftNodes[id].data.nodes.length > 0) {
const innerId = `${id}__${innerKey}`;
const childIds: string[] = [...craftNodes[id].data.nodes];
craftNodes[innerId] = {
id: innerId,
data: {
type: { resolvedName: 'Container' },
props: { tag: 'div' },
displayName: 'Container',
isCanvas: true,
parent: id,
nodes: childIds,
linkedNodes: {},
hidden: false,
custom: {},
},
events: { selected: false, hovered: false, dragged: false },
rules: { canDrag: () => true, canMoveIn: () => true, canMoveOut: () => true, canDrop: () => true },
};
for (const cid of childIds) {
if (craftNodes[cid]) craftNodes[cid].data.parent = innerId;
}
craftNodes[id].data.nodes = [];
craftNodes[id].data.linkedNodes = { [innerKey]: innerId };
}
return id;
};