puckeditor / puckeditor/puck

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

Open Beginner friendly
#1,663 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

ready type: bug 🐛
Dominant language
TypeScript
Stars
13.3k
Forks
977
Avg merge
5d 2h
Merged PRs (30d)
10

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at applyArrayDefaults in dist/index.js and dist/index.mjs, then trace its call from dispatchOp. Use the provided resolveFields reproduction with a missing array field and an AI update operation. Done means the non-array update is applied without the map TypeError or “Error applying operation, skipping...” message.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
ai, frontend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.