@contentful/experiences-core 1.29.0-dev-20250123T1244-eb6dcac.0 → 1.30.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,13 +10,15 @@ type EntityStoreArgs = {
10
10
  locale: string;
11
11
  };
12
12
  declare class EntityStore extends EntityStoreBase {
13
- private _experienceEntry;
13
+ private _experienceEntryFields;
14
+ private _experienceEntryId;
14
15
  private _unboundValues;
15
16
  private _usedComponentsWithDeepReferences;
16
17
  constructor(json: string);
17
18
  constructor({ experienceEntry, entities, locale }: EntityStoreArgs);
18
19
  getCurrentLocale(): string;
19
20
  get experienceEntryFields(): ExperienceFields | undefined;
21
+ get experienceEntryId(): string | undefined;
20
22
  get schemaVersion(): "2023-09-28" | undefined;
21
23
  get breakpoints(): {
22
24
  id: string;
@@ -54,7 +56,8 @@ declare class EntityStore extends EntityStoreBase {
54
56
  [k: string]: Asset<contentful.ChainModifiers, string>;
55
57
  };
56
58
  locale: string;
57
- _experienceEntry: ExperienceFields | undefined;
59
+ _experienceEntryFields: ExperienceFields | undefined;
60
+ _experienceEntryId: string | undefined;
58
61
  _unboundValues: Record<string, {
59
62
  value?: string | number | boolean | Record<any, any> | undefined;
60
63
  }> | undefined;
package/dist/index.js CHANGED
@@ -570,6 +570,7 @@ const builtInStyles = {
570
570
  defaultValue: '',
571
571
  validations: {
572
572
  format: 'URL',
573
+ bindingSourceType: ['entry', 'experience', 'manual'],
573
574
  },
574
575
  description: 'hyperlink for section or container',
575
576
  },
@@ -615,6 +616,9 @@ const optionalBuiltInStyles = {
615
616
  displayName: 'Image',
616
617
  type: 'Media',
617
618
  description: 'Image to display',
619
+ validations: {
620
+ bindingSourceType: ['entry', 'asset', 'manual'],
621
+ },
618
622
  },
619
623
  cfImageOptions: {
620
624
  displayName: 'Image options',
@@ -637,6 +641,9 @@ const optionalBuiltInStyles = {
637
641
  displayName: 'Background image',
638
642
  type: 'Media',
639
643
  description: 'Background image for component',
644
+ validations: {
645
+ bindingSourceType: ['entry', 'asset', 'manual'],
646
+ },
640
647
  },
641
648
  cfBackgroundImageOptions: {
642
649
  displayName: 'Background image options',
@@ -1556,7 +1563,7 @@ const detachExperienceStyles = (experience) => {
1556
1563
  }, {});
1557
1564
  // getting the breakpoint ids
1558
1565
  const breakpointIds = Object.keys(mediaQueriesTemplate);
1559
- const iterateOverTreeAndExtractStyles = ({ componentTree, dataSource, unboundValues, componentSettings, componentVariablesOverwrites, patternWrapper, }) => {
1566
+ const iterateOverTreeAndExtractStyles = ({ componentTree, dataSource, unboundValues, componentSettings, componentVariablesOverwrites, patternWrapper, wrappingPatternIds, }) => {
1560
1567
  // traversing the tree
1561
1568
  const queue = [];
1562
1569
  queue.push(...componentTree.children);
@@ -1573,6 +1580,10 @@ const detachExperienceStyles = (experience) => {
1573
1580
  usedComponents,
1574
1581
  });
1575
1582
  if (isPatternNode) {
1583
+ // When detecting a circular dependency among patterns, stop to avoid an infinite loop
1584
+ if (wrappingPatternIds.has(currentNode.definitionId)) {
1585
+ continue;
1586
+ }
1576
1587
  const patternEntry = usedComponents.find((component) => component.sys.id === currentNode.definitionId);
1577
1588
  if (!patternEntry || !('fields' in patternEntry)) {
1578
1589
  continue;
@@ -1613,6 +1624,7 @@ const detachExperienceStyles = (experience) => {
1613
1624
  componentVariablesOverwrites: currentNode.variables,
1614
1625
  // pass top-level pattern node to store instance-specific child styles for rendering
1615
1626
  patternWrapper: currentNode,
1627
+ wrappingPatternIds: new Set([...wrappingPatternIds, currentNode.definitionId]),
1616
1628
  });
1617
1629
  continue;
1618
1630
  }
@@ -1769,6 +1781,7 @@ const detachExperienceStyles = (experience) => {
1769
1781
  dataSource: experience.entityStore?.dataSource ?? {},
1770
1782
  unboundValues: experience.entityStore?.unboundValues ?? {},
1771
1783
  componentSettings: experience.entityStore?.experienceEntryFields?.componentSettings,
1784
+ wrappingPatternIds: new Set(experience.entityStore?.experienceEntryId ? [experience.entityStore.experienceEntryId] : []),
1772
1785
  });
1773
1786
  // once the whole tree was traversed, for each breakpoint, I aggregate the styles
1774
1787
  // for each generated className into one css string
@@ -3131,64 +3144,76 @@ class EditorModeEntityStore extends EditorEntityStore {
3131
3144
  }
3132
3145
  }
3133
3146
 
3134
- const gatherUsedComponentsWithDeepRefernces = (experienceEntryFields) => {
3135
- const usedComponentDeepReferences = [];
3147
+ const resolveDeepUsedComponents = ({ experienceEntryFields, parentComponents, }) => {
3148
+ const totalUsedComponents = [];
3136
3149
  const usedComponents = experienceEntryFields?.usedComponents;
3137
3150
  if (!usedComponents || usedComponents.length === 0) {
3138
3151
  return [];
3139
3152
  }
3140
3153
  for (const component of usedComponents) {
3141
3154
  if ('fields' in component) {
3142
- usedComponentDeepReferences.push(component);
3143
- usedComponentDeepReferences.push(...gatherUsedComponentsWithDeepRefernces(component.fields));
3155
+ totalUsedComponents.push(component);
3156
+ if (parentComponents.has(component.sys.id)) {
3157
+ continue;
3158
+ }
3159
+ totalUsedComponents.push(...resolveDeepUsedComponents({
3160
+ experienceEntryFields: component.fields,
3161
+ parentComponents: new Set([...parentComponents, component.sys.id]),
3162
+ }));
3144
3163
  }
3145
3164
  }
3146
- return usedComponentDeepReferences;
3165
+ return totalUsedComponents;
3147
3166
  };
3148
3167
 
3149
3168
  class EntityStore extends EntityStoreBase {
3150
3169
  constructor(options) {
3151
3170
  if (typeof options === 'string') {
3152
- const data = JSON.parse(options);
3153
- const { _experienceEntry, _unboundValues, locale, entryMap, assetMap } = data.entityStore;
3171
+ // For SSR/SSG, the entity store is created server-side and passed to the client as a serialised JSON.
3172
+ // So the properties in data.entityStore are equal to the attributes of this class (see `toJSON()`)
3173
+ const serializedAttributes = JSON.parse(options).entityStore;
3154
3174
  super({
3155
3175
  entities: [
3156
- ...Object.values(entryMap),
3157
- ...Object.values(assetMap),
3176
+ ...Object.values(serializedAttributes.entryMap),
3177
+ ...Object.values(serializedAttributes.assetMap),
3158
3178
  ],
3159
- locale,
3179
+ locale: serializedAttributes.locale,
3160
3180
  });
3161
- this._experienceEntry = _experienceEntry;
3162
- this._unboundValues = _unboundValues;
3163
- this._usedComponentsWithDeepReferences = gatherUsedComponentsWithDeepRefernces(this._experienceEntry);
3181
+ this._experienceEntryFields = serializedAttributes._experienceEntryFields;
3182
+ this._experienceEntryId = serializedAttributes._experienceEntryId;
3183
+ this._unboundValues = serializedAttributes._unboundValues;
3164
3184
  }
3165
3185
  else {
3166
3186
  const { experienceEntry, entities, locale } = options;
3167
- super({ entities, locale });
3168
- if (isExperienceEntry(experienceEntry)) {
3169
- this._experienceEntry = experienceEntry.fields;
3170
- this._unboundValues = experienceEntry.fields.unboundValues;
3171
- this._usedComponentsWithDeepReferences = gatherUsedComponentsWithDeepRefernces(this._experienceEntry);
3172
- }
3173
- else {
3187
+ if (!isExperienceEntry(experienceEntry)) {
3174
3188
  throw new Error('Provided entry is not experience entry');
3175
3189
  }
3190
+ super({ entities, locale });
3191
+ this._experienceEntryFields = experienceEntry.fields;
3192
+ this._experienceEntryId = experienceEntry.sys.id;
3193
+ this._unboundValues = experienceEntry.fields.unboundValues;
3176
3194
  }
3195
+ this._usedComponentsWithDeepReferences = resolveDeepUsedComponents({
3196
+ experienceEntryFields: this._experienceEntryFields,
3197
+ parentComponents: new Set([this._experienceEntryId]),
3198
+ });
3177
3199
  }
3178
3200
  getCurrentLocale() {
3179
3201
  return this.locale;
3180
3202
  }
3181
3203
  get experienceEntryFields() {
3182
- return this._experienceEntry;
3204
+ return this._experienceEntryFields;
3205
+ }
3206
+ get experienceEntryId() {
3207
+ return this._experienceEntryId;
3183
3208
  }
3184
3209
  get schemaVersion() {
3185
- return this._experienceEntry?.componentTree.schemaVersion;
3210
+ return this._experienceEntryFields?.componentTree.schemaVersion;
3186
3211
  }
3187
3212
  get breakpoints() {
3188
- return this._experienceEntry?.componentTree.breakpoints ?? [];
3213
+ return this._experienceEntryFields?.componentTree.breakpoints ?? [];
3189
3214
  }
3190
3215
  get dataSource() {
3191
- return this._experienceEntry?.dataSource ?? {};
3216
+ return this._experienceEntryFields?.dataSource ?? {};
3192
3217
  }
3193
3218
  get unboundValues() {
3194
3219
  return this._unboundValues ?? {};
@@ -3219,7 +3244,8 @@ class EntityStore extends EntityStoreBase {
3219
3244
  }
3220
3245
  toJSON() {
3221
3246
  return {
3222
- _experienceEntry: this._experienceEntry,
3247
+ _experienceEntryFields: this._experienceEntryFields,
3248
+ _experienceEntryId: this._experienceEntryId,
3223
3249
  _unboundValues: this._unboundValues,
3224
3250
  ...super.toJSON(),
3225
3251
  };
@@ -3544,22 +3570,22 @@ const fetchReferencedEntities = async ({ client, experienceEntry, locale, }) =>
3544
3570
  throw new Error('Failed to fetch experience entities. Provided "experienceEntry" does not match experience entry schema');
3545
3571
  }
3546
3572
  const deepReferences = gatherDeepReferencesFromExperienceEntry(experienceEntry);
3547
- const entryIds = [];
3548
- const assetIds = [];
3573
+ const entryIds = new Set();
3574
+ const assetIds = new Set();
3549
3575
  for (const dataBinding of Object.values(experienceEntry.fields.dataSource)) {
3550
3576
  if (!('sys' in dataBinding)) {
3551
3577
  continue;
3552
3578
  }
3553
3579
  if (dataBinding.sys.linkType === 'Entry') {
3554
- entryIds.push(dataBinding.sys.id);
3580
+ entryIds.add(dataBinding.sys.id);
3555
3581
  }
3556
3582
  if (dataBinding.sys.linkType === 'Asset') {
3557
- assetIds.push(dataBinding.sys.id);
3583
+ assetIds.add(dataBinding.sys.id);
3558
3584
  }
3559
3585
  }
3560
3586
  const [entriesResponse, assetsResponse] = await Promise.all([
3561
- fetchAllEntries({ client, ids: entryIds, locale }),
3562
- fetchAllAssets({ client, ids: assetIds, locale }),
3587
+ fetchAllEntries({ client, ids: [...entryIds], locale }),
3588
+ fetchAllAssets({ client, ids: [...assetIds], locale }),
3563
3589
  ]);
3564
3590
  const { autoFetchedReferentAssets, autoFetchedReferentEntries } = gatherAutoFetchedReferentsFromIncludes(deepReferences, entriesResponse);
3565
3591
  // Using client getEntries resolves all linked entry references, so we do not need to resolve entries in usedComponents
@@ -3580,6 +3606,68 @@ const fetchReferencedEntities = async ({ client, experienceEntry, locale, }) =>
3580
3606
  };
3581
3607
  };
3582
3608
 
3609
+ /**
3610
+ * The CMA client will automatically replace links with entry references.
3611
+ * As we're including all referenced pattern entries in usedComponents, this can lead
3612
+ * to a circular reference. This function replaces those with plain links inplace (!).
3613
+ *
3614
+ * @see https://github.com/contentful/contentful.js/issues/377
3615
+ */
3616
+ const removeCircularPatternReferences = (experienceEntry, _parentIds) => {
3617
+ const parentIds = _parentIds ?? new Set([experienceEntry.sys.id]);
3618
+ const usedComponents = experienceEntry.fields.usedComponents;
3619
+ const newUsedComponents = usedComponents?.reduce((acc, linkOrEntry) => {
3620
+ if (!('fields' in linkOrEntry)) {
3621
+ // It is a link, we're good
3622
+ return [...acc, linkOrEntry];
3623
+ }
3624
+ const entry = linkOrEntry;
3625
+ if (parentIds.has(entry.sys.id)) {
3626
+ // It is an entry that already occurred -> turn it into a link to remove the circularity
3627
+ const link = {
3628
+ sys: {
3629
+ id: entry.sys.id,
3630
+ linkType: 'Entry',
3631
+ type: 'Link',
3632
+ },
3633
+ };
3634
+ return [...acc, link];
3635
+ }
3636
+ // Remove circularity for its usedComponents as well (inplace)
3637
+ removeCircularPatternReferences(entry, new Set([...parentIds, entry.sys.id]));
3638
+ return [...acc, entry];
3639
+ }, []);
3640
+ // @ts-expect-error - type of usedComponents doesn't yet allow a mixed list of both links and entries
3641
+ experienceEntry.fields.usedComponents = newUsedComponents;
3642
+ };
3643
+ /**
3644
+ * The CMA client will automatically replace links with entry references if they are available.
3645
+ * While we're not fetching the data sources, a self reference would be replaced as the entry is
3646
+ * fetched. Any circuar reference in the object breaks SSR where we have to stringify the JSON.
3647
+ * This would fail if the object contains circular references.
3648
+ */
3649
+ const removeSelfReferencingDataSource = (experienceEntry) => {
3650
+ const dataSources = experienceEntry.fields.dataSource;
3651
+ const newDataSource = Object.entries(dataSources).reduce((acc, [key, linkOrEntry]) => {
3652
+ if ('fields' in linkOrEntry && linkOrEntry.sys.id === experienceEntry.sys.id) {
3653
+ const entry = linkOrEntry;
3654
+ acc[key] = {
3655
+ sys: {
3656
+ id: entry.sys.id,
3657
+ linkType: 'Entry',
3658
+ type: 'Link',
3659
+ },
3660
+ };
3661
+ }
3662
+ else {
3663
+ const link = linkOrEntry;
3664
+ acc[key] = link;
3665
+ }
3666
+ return acc;
3667
+ }, {});
3668
+ experienceEntry.fields.dataSource = newDataSource;
3669
+ };
3670
+
3583
3671
  const errorMessagesWhileFetching$1 = {
3584
3672
  experience: 'Failed to fetch experience',
3585
3673
  experienceReferences: 'Failed to fetch entities, referenced in experience',
@@ -3593,7 +3681,7 @@ const handleError$1 = (generalMessage, error) => {
3593
3681
  * @param {FetchBySlugParams} options - options to fetch the experience
3594
3682
  */
3595
3683
  async function fetchBySlug({ client, experienceTypeId, slug, localeCode, isEditorMode, }) {
3596
- //Be a no-op if in editor mode
3684
+ // Be a no-op if in editor mode
3597
3685
  if (isEditorMode)
3598
3686
  return;
3599
3687
  let experienceEntry = undefined;
@@ -3609,6 +3697,8 @@ async function fetchBySlug({ client, experienceTypeId, slug, localeCode, isEdito
3609
3697
  if (!experienceEntry) {
3610
3698
  throw new Error(`No experience entry with slug: ${slug} exists`);
3611
3699
  }
3700
+ removeCircularPatternReferences(experienceEntry);
3701
+ removeSelfReferencingDataSource(experienceEntry);
3612
3702
  try {
3613
3703
  const { entries, assets } = await fetchReferencedEntities({
3614
3704
  client,
@@ -3661,6 +3751,8 @@ async function fetchById({ client, experienceTypeId, id, localeCode, isEditorMod
3661
3751
  if (!experienceEntry) {
3662
3752
  throw new Error(`No experience entry with id: ${id} exists`);
3663
3753
  }
3754
+ removeCircularPatternReferences(experienceEntry);
3755
+ removeSelfReferencingDataSource(experienceEntry);
3664
3756
  try {
3665
3757
  const { entries, assets } = await fetchReferencedEntities({
3666
3758
  client,