fix(cms): stop recursive schema types from freezing the site editor
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$refis already on the current path,buildPropertystill emits the node (and a selector's option list) but skips its nestedproperties/items.- Array
itemsare passed tobuildPropertywith their$refintact, so the guard sees the item type. The{{…}}titleByis still read from the resolved definition. sectionHasMissingRequiredFieldusescachedResolveSchema, 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)toresolve-schema.test.ts. It uses a schema with the same structure asProduct/ImageObject/VideoObjectand asserts that the cycle is cut at the re-enteredProduct, 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
resolveSchemaover every manifest block (227) of an affected project'smeta.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:checkandlintare clean.
Screenshots/Demonstration
No visual change. The editor now opens instead of freezing.
How to Test
- Open the Site Editor for a project whose
.deco/meta.gen.jsonwas generated by@decocms/blocks-cli7.78.x and that has product shelf sections. - Open the Content panel and drill into a product shelf section.
- Expected: the panel loads and the fields render. Where
Productrefers 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 nestedpropertiesanditems, 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/VideoObjectshape, registered through the manifest so it exercises the full section resolution path; it never finished on the previous code and passes now.