Description
@puckeditor/plugin-ai's applyArrayDefaults helper assumes that every component instance has a populated value for every type: 'array' field declared in the component's fields config. When the merged { ...oldProps, ...newProps } doesn't contain a key for an array-typed field, the helper still attempts updatedProps[fieldName].map(...) and throws TypeError: Cannot read properties of undefined (reading 'map').
The error is caught by dispatchOp's outer try/catch, surfaces in the console as "Error applying operation, skipping...", and the entire AI operation is silently dropped. From the user's perspective the AI chat appears to do nothing.
This conflicts with Puck's own resolveFields pattern, which is the recommended way to build variant-based components: variants legitimately exclude some declared fields, so persisted props often don't contain every field the base config declares. Puck core handles this correctly everywhere else (insert/replace dispatches, render path, etc.).
Reproduces in @puckeditor/plugin-ai@0.6.0 and is still present on the latest canary 0.7.0-canary.75c0f12c (verified by reading dist/index.mjs at L6562–6589).
Environment
@puckeditor/plugin-ai version: 0.6.0
@puckeditor/core version: 0.21.2
- Browser: any (bug is plugin-internal, not browser-dependent)
- Bundler: Next.js 16 (webpack), but irrelevant — same crash in node
Steps to reproduce
- Define a component that declares a
type: 'array' top-level field but uses resolveFields to hide it for some variants:
const VARIANT_FIELDS: Record<string, string[]> = {
story: ['variant', 'title'], // 'items' hidden
list: ['variant', 'title', 'items'], // 'items' visible
};
const About: ComponentConfig = {
resolveFields: (data, { fields }) => {
const variant = data.props.variant ?? 'story';
const allowlist = VARIANT_FIELDS[variant];
return Object.fromEntries(
allowlist.map((key) => [key, fields[key]]).filter(([, v]) => v != null),
);
},
fields: {
variant: { type: 'select', options: [{ label: 'Story', value: 'story' }, { label: 'List', value: 'list' }] },
title: { type: 'text' },
items: {
type: 'array',
arrayFields: { text: { type: 'text' } },
defaultItemProps: { text: '' },
},
},
defaultProps: {
variant: 'story',
title: 'About',
// items omitted — variant is 'story', so 'items' is irrelevant
},
render: () => null,
};
- Seed (or otherwise persist) page data containing an instance of this component whose
props lack the items key — i.e. anything except a fresh insert via the sidebar:
{ "type": "About", "props": { "id": "about-1", "variant": "story", "title": "Hello" } }
-
Load the editor with the AI plugin enabled (createAiPlugin(...)).
-
In AI chat, ask for a change to any non-array field of that instance (e.g. "rename the title to X").
What happens
The AI returns a valid update op:
{ id: 'about-1', op: 'update', props: { title: 'X' } }
dispatchOp calls applyArrayDefaults(oldProps, newProps, config.components.About.fields). The merge is { id, variant: 'story', title: 'X' } — no items key. The loop sees fields.items.type === 'array' and runs updatedProps.items.map(...), throwing TypeError. The op is swallowed by dispatchOp's try/catch and logged as "Error applying operation, skipping...". The title is never updated.
What I expect to happen
applyArrayDefaults should treat a missing/non-array value as "nothing to normalize for this field" and leave it untouched, the same way the rest of Puck handles sparse props from resolveFields-driven variants.
Minimal fix in applyArrayDefaults (both dist/index.js and dist/index.mjs):
for (const fieldName in fields) {
const field = fields[fieldName];
if (field.type === "array") {
const arrayField = field;
const arrayFields = arrayField.arrayFields;
- updatedProps[fieldName] = updatedProps[fieldName].map(
+ const arrayValue = updatedProps[fieldName];
+ if (!Array.isArray(arrayValue)) {
+ continue;
+ }
+ updatedProps[fieldName] = arrayValue.map(
(item, index) => { /* … */ }
);
}
}
We are running this as a pnpm patch locally; happy to open a PR if useful.
Additional Media
[browser] Error applying operation, skipping... {
id: 'about-1',
op: 'update',
props: { sectionStyle: { /* … */ } }
} TypeError: can't access property "map", updatedProps[fieldName] is undefined
Description
@puckeditor/plugin-ai'sapplyArrayDefaultshelper assumes that every component instance has a populated value for everytype: 'array'field declared in the component'sfieldsconfig. When the merged{ ...oldProps, ...newProps }doesn't contain a key for an array-typed field, the helper still attemptsupdatedProps[fieldName].map(...)and throwsTypeError: Cannot read properties of undefined (reading 'map').The error is caught by
dispatchOp's outertry/catch, surfaces in the console as"Error applying operation, skipping...", and the entire AI operation is silently dropped. From the user's perspective the AI chat appears to do nothing.This conflicts with Puck's own
resolveFieldspattern, which is the recommended way to build variant-based components: variants legitimately exclude some declared fields, so persisted props often don't contain every field the base config declares. Puck core handles this correctly everywhere else (insert/replace dispatches, render path, etc.).Reproduces in
@puckeditor/plugin-ai@0.6.0and is still present on the latest canary0.7.0-canary.75c0f12c(verified by readingdist/index.mjsat L6562–6589).Environment
@puckeditor/plugin-aiversion: 0.6.0@puckeditor/coreversion: 0.21.2Steps to reproduce
type: 'array'top-level field but usesresolveFieldsto hide it for some variants:propslack theitemskey — i.e. anything except a fresh insert via the sidebar:{ "type": "About", "props": { "id": "about-1", "variant": "story", "title": "Hello" } }Load the editor with the AI plugin enabled (
createAiPlugin(...)).In AI chat, ask for a change to any non-array field of that instance (e.g. "rename the title to X").
What happens
The AI returns a valid
updateop:dispatchOpcallsapplyArrayDefaults(oldProps, newProps, config.components.About.fields). The merge is{ id, variant: 'story', title: 'X' }— noitemskey. The loop seesfields.items.type === 'array'and runsupdatedProps.items.map(...), throwingTypeError. The op is swallowed bydispatchOp'stry/catchand logged as"Error applying operation, skipping...". The title is never updated.What I expect to happen
applyArrayDefaultsshould treat a missing/non-array value as "nothing to normalize for this field" and leave it untouched, the same way the rest of Puck handles sparse props fromresolveFields-driven variants.Minimal fix in
applyArrayDefaults(bothdist/index.jsanddist/index.mjs):for (const fieldName in fields) { const field = fields[fieldName]; if (field.type === "array") { const arrayField = field; const arrayFields = arrayField.arrayFields; - updatedProps[fieldName] = updatedProps[fieldName].map( + const arrayValue = updatedProps[fieldName]; + if (!Array.isArray(arrayValue)) { + continue; + } + updatedProps[fieldName] = arrayValue.map( (item, index) => { /* … */ } ); } }We are running this as a
pnpm patchlocally; happy to open a PR if useful.Additional Media