@contentful/experiences-core 1.30.0-dev-20250124T1739-560b81b.0 → 1.30.0-dev-20250128T1255-64f728a.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
@@ -1563,7 +1563,7 @@ const detachExperienceStyles = (experience) => {
1563
1563
  }, {});
1564
1564
  // getting the breakpoint ids
1565
1565
  const breakpointIds = Object.keys(mediaQueriesTemplate);
1566
- const iterateOverTreeAndExtractStyles = ({ componentTree, dataSource, unboundValues, componentSettings, componentVariablesOverwrites, patternWrapper, }) => {
1566
+ const iterateOverTreeAndExtractStyles = ({ componentTree, dataSource, unboundValues, componentSettings, componentVariablesOverwrites, patternWrapper, wrappingPatternIds, }) => {
1567
1567
  // traversing the tree
1568
1568
  const queue = [];
1569
1569
  queue.push(...componentTree.children);
@@ -1580,6 +1580,10 @@ const detachExperienceStyles = (experience) => {
1580
1580
  usedComponents,
1581
1581
  });
1582
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
+ }
1583
1587
  const patternEntry = usedComponents.find((component) => component.sys.id === currentNode.definitionId);
1584
1588
  if (!patternEntry || !('fields' in patternEntry)) {
1585
1589
  continue;
@@ -1620,6 +1624,7 @@ const detachExperienceStyles = (experience) => {
1620
1624
  componentVariablesOverwrites: currentNode.variables,
1621
1625
  // pass top-level pattern node to store instance-specific child styles for rendering
1622
1626
  patternWrapper: currentNode,
1627
+ wrappingPatternIds: new Set([...wrappingPatternIds, currentNode.definitionId]),
1623
1628
  });
1624
1629
  continue;
1625
1630
  }
@@ -1776,6 +1781,7 @@ const detachExperienceStyles = (experience) => {
1776
1781
  dataSource: experience.entityStore?.dataSource ?? {},
1777
1782
  unboundValues: experience.entityStore?.unboundValues ?? {},
1778
1783
  componentSettings: experience.entityStore?.experienceEntryFields?.componentSettings,
1784
+ wrappingPatternIds: new Set(experience.entityStore?.experienceEntryId ? [experience.entityStore.experienceEntryId] : []),
1779
1785
  });
1780
1786
  // once the whole tree was traversed, for each breakpoint, I aggregate the styles
1781
1787
  // for each generated className into one css string
@@ -3138,64 +3144,76 @@ class EditorModeEntityStore extends EditorEntityStore {
3138
3144
  }
3139
3145
  }
3140
3146
 
3141
- const gatherUsedComponentsWithDeepRefernces = (experienceEntryFields) => {
3142
- const usedComponentDeepReferences = [];
3147
+ const resolveDeepUsedComponents = ({ experienceEntryFields, parentComponents, }) => {
3148
+ const totalUsedComponents = [];
3143
3149
  const usedComponents = experienceEntryFields?.usedComponents;
3144
3150
  if (!usedComponents || usedComponents.length === 0) {
3145
3151
  return [];
3146
3152
  }
3147
3153
  for (const component of usedComponents) {
3148
3154
  if ('fields' in component) {
3149
- usedComponentDeepReferences.push(component);
3150
- 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
+ }));
3151
3163
  }
3152
3164
  }
3153
- return usedComponentDeepReferences;
3165
+ return totalUsedComponents;
3154
3166
  };
3155
3167
 
3156
3168
  class EntityStore extends EntityStoreBase {
3157
3169
  constructor(options) {
3158
3170
  if (typeof options === 'string') {
3159
- const data = JSON.parse(options);
3160
- 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;
3161
3174
  super({
3162
3175
  entities: [
3163
- ...Object.values(entryMap),
3164
- ...Object.values(assetMap),
3176
+ ...Object.values(serializedAttributes.entryMap),
3177
+ ...Object.values(serializedAttributes.assetMap),
3165
3178
  ],
3166
- locale,
3179
+ locale: serializedAttributes.locale,
3167
3180
  });
3168
- this._experienceEntry = _experienceEntry;
3169
- this._unboundValues = _unboundValues;
3170
- this._usedComponentsWithDeepReferences = gatherUsedComponentsWithDeepRefernces(this._experienceEntry);
3181
+ this._experienceEntryFields = serializedAttributes._experienceEntryFields;
3182
+ this._experienceEntryId = serializedAttributes._experienceEntryId;
3183
+ this._unboundValues = serializedAttributes._unboundValues;
3171
3184
  }
3172
3185
  else {
3173
3186
  const { experienceEntry, entities, locale } = options;
3174
- super({ entities, locale });
3175
- if (isExperienceEntry(experienceEntry)) {
3176
- this._experienceEntry = experienceEntry.fields;
3177
- this._unboundValues = experienceEntry.fields.unboundValues;
3178
- this._usedComponentsWithDeepReferences = gatherUsedComponentsWithDeepRefernces(this._experienceEntry);
3179
- }
3180
- else {
3187
+ if (!isExperienceEntry(experienceEntry)) {
3181
3188
  throw new Error('Provided entry is not experience entry');
3182
3189
  }
3190
+ super({ entities, locale });
3191
+ this._experienceEntryFields = experienceEntry.fields;
3192
+ this._experienceEntryId = experienceEntry.sys.id;
3193
+ this._unboundValues = experienceEntry.fields.unboundValues;
3183
3194
  }
3195
+ this._usedComponentsWithDeepReferences = resolveDeepUsedComponents({
3196
+ experienceEntryFields: this._experienceEntryFields,
3197
+ parentComponents: new Set([this._experienceEntryId]),
3198
+ });
3184
3199
  }
3185
3200
  getCurrentLocale() {
3186
3201
  return this.locale;
3187
3202
  }
3188
3203
  get experienceEntryFields() {
3189
- return this._experienceEntry;
3204
+ return this._experienceEntryFields;
3205
+ }
3206
+ get experienceEntryId() {
3207
+ return this._experienceEntryId;
3190
3208
  }
3191
3209
  get schemaVersion() {
3192
- return this._experienceEntry?.componentTree.schemaVersion;
3210
+ return this._experienceEntryFields?.componentTree.schemaVersion;
3193
3211
  }
3194
3212
  get breakpoints() {
3195
- return this._experienceEntry?.componentTree.breakpoints ?? [];
3213
+ return this._experienceEntryFields?.componentTree.breakpoints ?? [];
3196
3214
  }
3197
3215
  get dataSource() {
3198
- return this._experienceEntry?.dataSource ?? {};
3216
+ return this._experienceEntryFields?.dataSource ?? {};
3199
3217
  }
3200
3218
  get unboundValues() {
3201
3219
  return this._unboundValues ?? {};
@@ -3226,7 +3244,8 @@ class EntityStore extends EntityStoreBase {
3226
3244
  }
3227
3245
  toJSON() {
3228
3246
  return {
3229
- _experienceEntry: this._experienceEntry,
3247
+ _experienceEntryFields: this._experienceEntryFields,
3248
+ _experienceEntryId: this._experienceEntryId,
3230
3249
  _unboundValues: this._unboundValues,
3231
3250
  ...super.toJSON(),
3232
3251
  };
@@ -3587,6 +3606,41 @@ const fetchReferencedEntities = async ({ client, experienceEntry, locale, }) =>
3587
3606
  };
3588
3607
  };
3589
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
+
3590
3644
  const errorMessagesWhileFetching$1 = {
3591
3645
  experience: 'Failed to fetch experience',
3592
3646
  experienceReferences: 'Failed to fetch entities, referenced in experience',
@@ -3600,7 +3654,7 @@ const handleError$1 = (generalMessage, error) => {
3600
3654
  * @param {FetchBySlugParams} options - options to fetch the experience
3601
3655
  */
3602
3656
  async function fetchBySlug({ client, experienceTypeId, slug, localeCode, isEditorMode, }) {
3603
- //Be a no-op if in editor mode
3657
+ // Be a no-op if in editor mode
3604
3658
  if (isEditorMode)
3605
3659
  return;
3606
3660
  let experienceEntry = undefined;
@@ -3616,6 +3670,7 @@ async function fetchBySlug({ client, experienceTypeId, slug, localeCode, isEdito
3616
3670
  if (!experienceEntry) {
3617
3671
  throw new Error(`No experience entry with slug: ${slug} exists`);
3618
3672
  }
3673
+ removeCircularPatternReferences(experienceEntry);
3619
3674
  try {
3620
3675
  const { entries, assets } = await fetchReferencedEntities({
3621
3676
  client,
@@ -3668,6 +3723,7 @@ async function fetchById({ client, experienceTypeId, id, localeCode, isEditorMod
3668
3723
  if (!experienceEntry) {
3669
3724
  throw new Error(`No experience entry with id: ${id} exists`);
3670
3725
  }
3726
+ removeCircularPatternReferences(experienceEntry);
3671
3727
  try {
3672
3728
  const { entries, assets } = await fetchReferencedEntities({
3673
3729
  client,