Skip to content

plugin-ai: applyArrayDefaults crashes on resolveFields-hidden array fields, silently drops AI ops #1663

@yoavaviram

Description

@yoavaviram

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

  1. 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,
};
  1. 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" } }
  1. Load the editor with the AI plugin enabled (createAiPlugin(...)).

  2. 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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions