fix(cms): stop recursive schema types from freezing the site editor

Merged
GU
4d ago
+98-26

What is this contribution about?

After a storefront bumps @decocms/* from 7.71.x to 7.78.x, its regenerated .deco/meta.gen.json emits recursive commerce types as named definitions. Earlier versions inlined them. In these definitions, …/commerce.ts@Product references itself 6 times (for example isVariantOf.hasVariant: Product[] and isRelatedTo), and @ImageObject and @VideoObject reference each other. Opening the Site Editor on such a project froze the browser tab: first Chrome's "Page unresponsive" dialog, then "Aw, Snap! Error code 5" (out of memory).

The cause is in buildProperty in resolve-schema.ts. Its seen cycle guard only gated union branch materialization. Plain structural descent (nested properties and array items) re-expanded every re-entered $ref until MAX_STRUCTURE_DEPTH (32). Array items.$ref was resolved before the recursive call, so its key never reached seen. With 3 or more self-references per level, the expansion never finishes in practice. The section list runs resolveSchema per row to show the required-field dot, so the hang started as soon as the panel opened.

Changes:

  • cyclicUnion → cyclicRef. When a $ref is already on the current path, buildProperty still emits the node (and a selector's option list) but skips its nested properties/items.
  • Array items are passed to buildProperty with their $ref intact, so the guard sees the item type. The {{…}} titleBy is still read from the resolved definition.
  • sectionHasMissingRequiredField uses cachedResolveSchema, so a page with many product shelves resolves each section type once.

How did you verify your code works?

  • Added resolveSchema – recursive data types (hang guard) to resolve-schema.test.ts. It uses a schema with the same structure as Product/ImageObject/VideoObject and asserts that the cycle is cut at the re-entered Product, both through an array item and through a nested object's array. On the previous code this test never finishes (killed after 30s); with the fix it passes.
  • Ran the real resolveSchema over every manifest block (227) of an affected project's meta.gen.json, before and after its dependency bump:
    • pre-bump meta: ~70–90ms total, with and without this change.
    • post-bump meta, old code: hung for more than 60s on the first section with a product loader.
    • post-bump meta, this change: ~0.6s total, at most ~130ms for a single section.
  • bun test src/components/sections-editor/: 865 pass. bun run verify: 10883 pass, 0 fail. fmt:check and lint are clean.

Screenshots/Demonstration

No visual change. The editor now opens instead of freezing.

How to Test

  1. Open the Site Editor for a project whose .deco/meta.gen.json was generated by @decocms/blocks-cli 7.78.x and that has product shelf sections.
  2. Open the Content panel and drill into a product shelf section.
  3. Expected: the panel loads and the fields render. Where Product refers back to itself, the field shows as an object with no nested fields instead of expanding again.

Review Checklist

  • PR title is clear and descriptive
  • Changes are tested and working
  • Documentation is updated (if needed)
  • No breaking changes

🤖 Generated with Claude Code


Summary by cubic

Fixes the Site Editor freezing on schema types that reference themselves recursively, which started after a @decocms/* bump regenerated .deco/meta.gen.json with named recursive definitions like Product (self-referencing via isVariantOf.hasVariant: Product[]) and mutual references between ImageObject and VideoObject.

The cycle guard previously only gated union branch materialization, so plain property and array item descent re-expanded every re-entered $ref until the depth cap — enough to hang the browser tab when the section list resolved its schemas.

Changes

  • $refs already on the current path now emit the node but skip nested properties and items, for arrays and unions alike.
  • Array item $refs are passed through to the cycle guard instead of being resolved first.
  • The section list's required-field check now uses cachedResolveSchema, so each section type resolves once per page.
  • Regression test covers the Product/ImageObject/VideoObject shape, registered through the manifest so it exercises the full section resolution path; it never finished on the previous code and passes now.