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:
@@ -12,6 +12,18 @@ import { useSiteDesign, SiteDesign } from './SiteDesignContext';
|
||||
* because the shell claims to be a canvas but its render ignores `nodes`. */
|
||||
const CANVAS_TYPES = new Set<string>(['Container']);
|
||||
|
||||
/** Shells that wrap their content in a single <Element id="<key>" is={Container}>.
|
||||
* When the AI puts content directly under one of these, the children end up
|
||||
* orphaned (the shell ignores data.nodes — it renders via the linkedNode) and
|
||||
* Craft.js auto-creates the linkedNode at render time with a botched type
|
||||
* field, which then crashes toNodeTree. Pre-create the linkedNode ourselves
|
||||
* to keep the state shape Craft.js expects. */
|
||||
const SHELL_INNER: Record<string, string> = {
|
||||
Section: 'section-inner',
|
||||
BackgroundSection: 'bg-section-inner',
|
||||
FormContainer: 'form-inner',
|
||||
};
|
||||
|
||||
interface PageContextValue {
|
||||
pages: PageData[];
|
||||
headerPage: PageData;
|
||||
@@ -340,9 +352,56 @@ export const PageProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||
if (!cur.columns || cur.columns !== childIds.length) cur.columns = childIds.length;
|
||||
(nodes[id] as any).props = cur;
|
||||
}
|
||||
// Section/BackgroundSection/FormContainer each render a single
|
||||
// <Element id="<key>" is={Container} canvas> ... </Element>. If the AI
|
||||
// nests content as direct children, Craft.js will auto-create the
|
||||
// linkedNode on first render — and store its type as the Container
|
||||
// component class rather than {resolvedName:'Container'}, which then
|
||||
// crashes toNodeTree with "type (undefined) does not exist in resolver".
|
||||
// Pre-create the linkedNode ourselves with the correct serialized type
|
||||
// so Craft.js never has to materialize it.
|
||||
const innerKey = SHELL_INNER[typeName ?? ''];
|
||||
if (innerKey && childIds.length > 0) {
|
||||
const innerId = `${id}__${innerKey}`;
|
||||
nodes[innerId] = {
|
||||
type: { resolvedName: 'Container' },
|
||||
isCanvas: true,
|
||||
props: { tag: 'div' },
|
||||
displayName: 'Container',
|
||||
custom: {},
|
||||
hidden: false,
|
||||
parent: id,
|
||||
nodes: [...childIds],
|
||||
linkedNodes: {},
|
||||
};
|
||||
for (const cid of childIds) {
|
||||
if (nodes[cid]) (nodes[cid] as any).parent = innerId;
|
||||
}
|
||||
(nodes[id] as any).nodes = [];
|
||||
(nodes[id] as any).linkedNodes = { [innerKey]: innerId };
|
||||
}
|
||||
return id;
|
||||
};
|
||||
const rootId = walk(tree, null);
|
||||
// DIAGNOSTIC: dump the produced state + scan for type=undefined nodes
|
||||
// so when the resolver invariant fires we can see which node is bad.
|
||||
try {
|
||||
const w = window as any;
|
||||
w.__sitesmithLastTree = tree;
|
||||
w.__sitesmithLastState = nodes;
|
||||
const bad: Array<{id:string; node:any}> = [];
|
||||
for (const [nid, n] of Object.entries(nodes)) {
|
||||
const t = (n as any).type;
|
||||
const name = t && typeof t === 'object' ? t.resolvedName : t;
|
||||
if (!name) bad.push({ id: nid, node: n });
|
||||
}
|
||||
if (bad.length > 0) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[SITESMITH] nodes with undefined type:', bad);
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[SITESMITH] treeToState produced', Object.keys(nodes).length, 'nodes; rootId=', rootId);
|
||||
} catch (_e) { /* diagnostic only */ }
|
||||
// Craft.js deserialize requires the root node keyed as 'ROOT'
|
||||
if (rootId !== 'ROOT') {
|
||||
nodes['ROOT'] = nodes[rootId];
|
||||
|
||||
Reference in New Issue
Block a user