@contentful/experiences-core 1.42.4-prerelease-20250702T1207-37b3bfc.0 → 2.0.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.
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import { z, ZodIssueCode } from 'zod';
2
2
  import { omit, isArray, uniqBy } from 'lodash-es';
3
3
  import md5 from 'md5';
4
4
  import { BLOCKS } from '@contentful/rich-text-types';
5
+ import { create } from 'zustand';
5
6
 
6
7
  const INCOMING_EVENTS = {
7
8
  RequestEditorMode: 'requestEditorMode',
@@ -114,14 +115,6 @@ const CF_STYLE_ATTRIBUTES = [
114
115
  'cfTextBold',
115
116
  'cfTextItalic',
116
117
  'cfTextUnderline',
117
- // For backwards compatibility
118
- // we need to keep those in this constant array
119
- // so that omit() in <VisualEditorBlock> and <CompositionBlock>
120
- // can filter them out and not pass as props
121
- 'cfBackgroundImageScaling',
122
- 'cfBackgroundImageAlignment',
123
- 'cfBackgroundImageAlignmentVertical',
124
- 'cfBackgroundImageAlignmentHorizontal',
125
118
  ];
126
119
  const EMPTY_CONTAINER_HEIGHT = '80px';
127
120
  const DEFAULT_IMAGE_WIDTH = '500px';
@@ -1066,7 +1059,13 @@ const ParameterDefinitionSchema = z.object({
1066
1059
  }),
1067
1060
  })
1068
1061
  .optional(),
1069
- contentTypes: z.array(z.string()),
1062
+ contentTypes: z.record(z.string(), z.object({
1063
+ sys: z.object({
1064
+ type: z.literal('Link'),
1065
+ id: z.string(),
1066
+ linkType: z.enum(['ContentType']),
1067
+ }),
1068
+ })),
1070
1069
  passToNodes: z.array(PassToNodeSchema).optional(),
1071
1070
  });
1072
1071
  const ParameterDefinitionsSchema = z.record(propertyKeySchema, ParameterDefinitionSchema);
@@ -1086,7 +1085,7 @@ const ComponentSettingsSchema = z
1086
1085
  variableDefinitions: ComponentVariablesSchema,
1087
1086
  thumbnailId: z.enum(THUMBNAIL_IDS).optional(),
1088
1087
  category: z.string().max(50, 'Category must contain at most 50 characters').optional(),
1089
- prebindingDefinitions: z.array(PrebindingDefinitionSchema).length(1).optional(),
1088
+ prebindingDefinitions: z.array(PrebindingDefinitionSchema).max(1).optional(),
1090
1089
  })
1091
1090
  .strict();
1092
1091
  z.object({
@@ -1457,6 +1456,24 @@ propertyName, resolveDesignTokens = true) => {
1457
1456
  return valuesByBreakpoint;
1458
1457
  }
1459
1458
  };
1459
+ /** Overwrites the default value breakpoint by breakpoint. If a breakpoint
1460
+ * is not overwritten, it will fall back to the default. */
1461
+ function mergeDesignValuesByBreakpoint(defaultValue, overwriteValue) {
1462
+ if (!defaultValue || !overwriteValue) {
1463
+ return defaultValue ?? overwriteValue;
1464
+ }
1465
+ const mergedValuesByBreakpoint = { ...defaultValue.valuesByBreakpoint };
1466
+ for (const [breakpointId, value] of Object.entries(overwriteValue.valuesByBreakpoint)) {
1467
+ if (!isValidBreakpointValue(value)) {
1468
+ continue;
1469
+ }
1470
+ mergedValuesByBreakpoint[breakpointId] = value;
1471
+ }
1472
+ return {
1473
+ type: 'DesignValue',
1474
+ valuesByBreakpoint: mergedValuesByBreakpoint,
1475
+ };
1476
+ }
1460
1477
 
1461
1478
  const CF_DEBUG_KEY = 'cf_debug';
1462
1479
  /**
@@ -1586,11 +1603,19 @@ const getElementCoordinates = (element) => {
1586
1603
  });
1587
1604
  };
1588
1605
 
1589
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1590
1606
  const isLinkToAsset = (variable) => {
1591
- if (!variable)
1607
+ if (variable === null || typeof variable !== 'object')
1608
+ return false;
1609
+ // The `'prop' in` pattern is informing TypeScript of the object shape, no need to cast `as`.
1610
+ if (!('sys' in variable))
1611
+ return false;
1612
+ if (variable.sys === null || typeof variable.sys !== 'object')
1592
1613
  return false;
1593
- if (typeof variable !== 'object')
1614
+ if (!('linkType' in variable.sys))
1615
+ return false;
1616
+ if (!('id' in variable.sys))
1617
+ return false;
1618
+ if (!('type' in variable.sys))
1594
1619
  return false;
1595
1620
  return (variable.sys?.linkType === 'Asset' &&
1596
1621
  typeof variable.sys?.id === 'string' &&
@@ -1598,13 +1623,33 @@ const isLinkToAsset = (variable) => {
1598
1623
  variable.sys?.type === 'Link');
1599
1624
  };
1600
1625
 
1626
+ const isLinkToEntry = (variable) => {
1627
+ if (variable === null || typeof variable !== 'object')
1628
+ return false;
1629
+ // The `'prop' in` pattern is informing TypeScript of the object shape, no need to cast `as`.
1630
+ if (!('sys' in variable))
1631
+ return false;
1632
+ if (variable.sys === null || typeof variable.sys !== 'object')
1633
+ return false;
1634
+ if (!('linkType' in variable.sys))
1635
+ return false;
1636
+ if (!('id' in variable.sys))
1637
+ return false;
1638
+ if (!('type' in variable.sys))
1639
+ return false;
1640
+ return (variable.sys?.linkType === 'Entry' &&
1641
+ typeof variable.sys?.id === 'string' &&
1642
+ !!variable.sys?.id &&
1643
+ variable.sys?.type === 'Link');
1644
+ };
1645
+
1601
1646
  const isLink = (maybeLink) => {
1602
1647
  if (maybeLink === null)
1603
1648
  return false;
1604
1649
  if (typeof maybeLink !== 'object')
1605
1650
  return false;
1606
1651
  const link = maybeLink;
1607
- return Boolean(link.sys?.id) && link.sys?.type === 'Link';
1652
+ return Boolean(link.sys?.id) && link.sys?.type === 'Link' && Boolean(link.sys?.linkType);
1608
1653
  };
1609
1654
 
1610
1655
  /**
@@ -1831,11 +1876,6 @@ const transformVisibility = (value) => {
1831
1876
  // Don't explicitly set anything when visible to not overwrite values like `grid` or `flex`.
1832
1877
  return {};
1833
1878
  };
1834
- // TODO: Remove in next major version v2 since the change is 17 months old
1835
- // Keep this for backwards compatibility - deleting this would be a breaking change
1836
- // because existing components on a users experience will have the width value as fill
1837
- // rather than 100%
1838
- const transformFill = (value) => (value === 'fill' ? '100%' : value);
1839
1879
  const transformGridColumn = (span) => {
1840
1880
  if (!span) {
1841
1881
  return {};
@@ -1880,34 +1920,6 @@ const transformBackgroundImage = (cfBackgroundImageUrl, cfBackgroundImageOptions
1880
1920
  return;
1881
1921
  }
1882
1922
  let [horizontalAlignment, verticalAlignment] = alignment.trim().split(/\s+/, 2);
1883
- // Special case for handling single values
1884
- // for backwards compatibility with single values 'right','left', 'center', 'top','bottom'
1885
- if (horizontalAlignment && !verticalAlignment) {
1886
- const singleValue = horizontalAlignment;
1887
- switch (singleValue) {
1888
- case 'left':
1889
- horizontalAlignment = 'left';
1890
- verticalAlignment = 'center';
1891
- break;
1892
- case 'right':
1893
- horizontalAlignment = 'right';
1894
- verticalAlignment = 'center';
1895
- break;
1896
- case 'center':
1897
- horizontalAlignment = 'center';
1898
- verticalAlignment = 'center';
1899
- break;
1900
- case 'top':
1901
- horizontalAlignment = 'center';
1902
- verticalAlignment = 'top';
1903
- break;
1904
- case 'bottom':
1905
- horizontalAlignment = 'center';
1906
- verticalAlignment = 'bottom';
1907
- break;
1908
- // just fall down to the normal validation logic for horiz and vert
1909
- }
1910
- }
1911
1923
  const isHorizontalValid = ['left', 'right', 'center'].includes(horizontalAlignment);
1912
1924
  const isVerticalValid = ['top', 'bottom', 'center'].includes(verticalAlignment);
1913
1925
  horizontalAlignment = isHorizontalValid ? horizontalAlignment : 'left';
@@ -1992,8 +2004,8 @@ const buildCfStyles = (values) => {
1992
2004
  margin: values.cfMargin,
1993
2005
  padding: values.cfPadding,
1994
2006
  backgroundColor: values.cfBackgroundColor,
1995
- width: transformFill(values.cfWidth || values.cfImageOptions?.width),
1996
- height: transformFill(values.cfHeight || values.cfImageOptions?.height),
2007
+ width: values.cfWidth || values.cfImageOptions?.width,
2008
+ height: values.cfHeight || values.cfImageOptions?.height,
1997
2009
  maxWidth: values.cfMaxWidth,
1998
2010
  ...transformGridColumn(values.cfColumnSpan),
1999
2011
  ...transformBorderStyle(values.cfBorder),
@@ -2216,7 +2228,7 @@ const detachExperienceStyles = (experience) => {
2216
2228
  * {
2217
2229
  * desktop: {
2218
2230
  * cfMargin: '1px',
2219
- * cfWidth: 'fill',
2231
+ * cfWidth: '100%',
2220
2232
  * cfBackgroundImageUrl: 'https://example.com/image.jpg'
2221
2233
  * //...
2222
2234
  * }
@@ -2385,9 +2397,10 @@ const resolveComponentVariablesOverwrites = ({ patternNode, wrapperComponentVari
2385
2397
  const overwritingValue = wrapperComponentVariablesOverwrites?.[propertyValue.key];
2386
2398
  // Property definition from the parent pattern
2387
2399
  const propertyDefinition = wrapperComponentSettings?.variableDefinitions?.[propertyValue.key];
2400
+ const defaultValue = propertyDefinition?.defaultValue;
2388
2401
  // The overwriting value is either a custom value from the experience or default value from a
2389
2402
  // wrapping pattern node that got trickled down to this nesting level.
2390
- resolvedValues[propertyName] = overwritingValue ?? propertyDefinition?.defaultValue;
2403
+ resolvedValues[propertyName] = mergeDefaultAndOverwriteValues(defaultValue, overwritingValue);
2391
2404
  }
2392
2405
  else {
2393
2406
  // Keep raw values
@@ -2470,15 +2483,15 @@ const resolveBackgroundImageBinding = ({ variableData, getBoundEntityById, dataS
2470
2483
  // @ts-expect-error TODO: Types coming from validations erroneously assume that `defaultValue` can be a primitive value (e.g. string or number)
2471
2484
  const defaultValueKey = variableDefinition.defaultValue?.key;
2472
2485
  const defaultValue = unboundValues[defaultValueKey].value;
2473
- const userSetValue = componentVariablesOverwrites?.[variableDefinitionKey];
2474
- // userSetValue is a ComponentValue we can safely return the default value
2475
- if (!userSetValue || userSetValue.type === 'ComponentValue') {
2486
+ const overwriteValue = componentVariablesOverwrites?.[variableDefinitionKey];
2487
+ // overwriteValue is a ComponentValue we can safely return the default value
2488
+ if (!overwriteValue || overwriteValue.type === 'ComponentValue') {
2476
2489
  return defaultValue;
2477
2490
  }
2478
- // at this point userSetValue will either be type of 'DesignValue' or 'BoundValue'
2491
+ // at this point overwriteValue will either be type of 'DesignValue' or 'BoundValue'
2479
2492
  // so we recursively run resolution again to resolve it
2480
2493
  const resolvedValue = resolveBackgroundImageBinding({
2481
- variableData: userSetValue,
2494
+ variableData: overwriteValue,
2482
2495
  getBoundEntityById,
2483
2496
  dataSource,
2484
2497
  unboundValues,
@@ -2631,10 +2644,10 @@ const indexByBreakpoint = ({ variables, breakpointIds, getBoundEntityById, unbou
2631
2644
  let resolvedVariableData = variableData;
2632
2645
  if (variableData.type === 'ComponentValue') {
2633
2646
  const variableDefinition = componentSettings?.variableDefinitions[variableData.key];
2634
- if (variableDefinition.group === 'style' && variableDefinition.defaultValue !== undefined) {
2635
- const overrideVariableData = componentVariablesOverwrites?.[variableData.key];
2636
- resolvedVariableData =
2637
- overrideVariableData || variableDefinition.defaultValue;
2647
+ const defaultValue = variableDefinition.defaultValue;
2648
+ if (variableDefinition.group === 'style' && defaultValue !== undefined) {
2649
+ const overwriteVariableData = componentVariablesOverwrites?.[variableData.key];
2650
+ resolvedVariableData = mergeDefaultAndOverwriteValues(defaultValue, overwriteVariableData);
2638
2651
  }
2639
2652
  }
2640
2653
  if (resolvedVariableData.type !== 'DesignValue') {
@@ -2672,6 +2685,12 @@ const flattenDesignTokenRegistry = (designTokenRegistry) => {
2672
2685
  };
2673
2686
  }, {});
2674
2687
  };
2688
+ function mergeDefaultAndOverwriteValues(defaultValue, overwriteValue) {
2689
+ if (defaultValue?.type === 'DesignValue' && overwriteValue?.type === 'DesignValue') {
2690
+ return mergeDesignValuesByBreakpoint(defaultValue, overwriteValue);
2691
+ }
2692
+ return overwriteValue ?? defaultValue;
2693
+ }
2675
2694
 
2676
2695
  /**
2677
2696
  * Turns a condition like `<768px` or `>1024px` into a media query rule.
@@ -2767,7 +2786,9 @@ const transformRichText = (entryOrAsset, entityStore, path) => {
2767
2786
  }
2768
2787
  if (typeof value === 'object' && value.nodeType === BLOCKS.DOCUMENT) {
2769
2788
  // resolve any links to assets/entries/hyperlinks
2770
- const richTextDocument = value;
2789
+ // we need to clone, as we want to keep the original Entity in the EntityStore intact,
2790
+ // and resolveLinks() is mutating the node object.
2791
+ const richTextDocument = structuredClone(value);
2771
2792
  resolveLinks(richTextDocument, entityStore);
2772
2793
  return richTextDocument;
2773
2794
  }
@@ -2881,17 +2902,37 @@ const transformMedia = (asset, variables, resolveDesignValue, variableName, path
2881
2902
  return asset.fields.file?.url;
2882
2903
  };
2883
2904
 
2884
- const isAsset = (value) => {
2905
+ const isExperienceEntry = (entry) => {
2906
+ return (entry?.sys?.type === 'Entry' &&
2907
+ !!entry.fields?.title &&
2908
+ !!entry.fields?.slug &&
2909
+ !!entry.fields?.componentTree &&
2910
+ Array.isArray(entry.fields.componentTree.breakpoints) &&
2911
+ Array.isArray(entry.fields.componentTree.children) &&
2912
+ typeof entry.fields.componentTree.schemaVersion === 'string');
2913
+ };
2914
+ const isPatternEntry = (entry) => {
2915
+ return isExperienceEntry(entry) && !!entry.fields?.componentSettings; // signals that this is pattern (not experience) entry
2916
+ };
2917
+ const isEntry = (value) => {
2885
2918
  return (null !== value &&
2886
2919
  typeof value === 'object' &&
2887
2920
  'sys' in value &&
2888
- value.sys?.type === 'Asset');
2921
+ value.sys?.type === 'Entry');
2889
2922
  };
2890
- const isEntry = (value) => {
2923
+ const isAsset = (value) => {
2891
2924
  return (null !== value &&
2892
2925
  typeof value === 'object' &&
2893
2926
  'sys' in value &&
2894
- value.sys?.type === 'Entry');
2927
+ value.sys?.type === 'Asset');
2928
+ };
2929
+ /**
2930
+ * Checks if the values is an array of links.
2931
+ * Note: we use convention where empty arrays are considered valid "arrays of links"
2932
+ * as they don't contradict the type definition.
2933
+ */
2934
+ const isArrayOfLinks = (value) => {
2935
+ return Array.isArray(value) && value.every((item) => isLink(item));
2895
2936
  };
2896
2937
 
2897
2938
  function getResolvedEntryFromLink(entryOrAsset, path, entityStore) {
@@ -2901,7 +2942,8 @@ function getResolvedEntryFromLink(entryOrAsset, path, entityStore) {
2901
2942
  else if (!isEntry(entryOrAsset)) {
2902
2943
  throw new Error(`Expected an Entry or Asset, but got: ${JSON.stringify(entryOrAsset)}`);
2903
2944
  }
2904
- const value = get(entryOrAsset, path.split('/').slice(2, -1));
2945
+ const fieldName = path.split('/').slice(2, -1);
2946
+ const value = get(entryOrAsset, fieldName);
2905
2947
  let resolvedEntity;
2906
2948
  if (isAsset(value) || isEntry(value)) {
2907
2949
  // In some cases, reference fields are already resolved
@@ -2910,74 +2952,67 @@ function getResolvedEntryFromLink(entryOrAsset, path, entityStore) {
2910
2952
  else if (value?.sys.type === 'Link') {
2911
2953
  // Look up the reference in the entity store
2912
2954
  resolvedEntity = entityStore.getEntityFromLink(value);
2913
- if (!resolvedEntity) {
2914
- return;
2915
- }
2916
2955
  }
2917
2956
  else {
2918
- console.warn(`Expected a link to a reference, but got: ${JSON.stringify(value)}`);
2957
+ console.warn(`When attempting to follow link in field '${fieldName}' of entity, the value is expected to be a link, but got: ${JSON.stringify(value)}`, { entity: entryOrAsset });
2958
+ return;
2959
+ }
2960
+ // no need to make structuredClone(entityStore.getEntityFromLink(value)) because
2961
+ // we provide component with the original Object.frozen object of the entity.
2962
+ // As we don't resolve L3 and don't mutate the entity before returning anymore,
2963
+ // we don't need to make a copy of the entity. And even provide better referential integrity
2964
+ // for the component for the same entity.
2965
+ if (!resolvedEntity) {
2919
2966
  return;
2920
2967
  }
2921
- //resolve any embedded links - we currently only support 2 levels deep
2922
- const fields = resolvedEntity.fields || {};
2923
- Object.entries(fields).forEach(([fieldKey, field]) => {
2924
- if (field && field.sys?.type === 'Link') {
2925
- const entity = entityStore.getEntityFromLink(field);
2926
- if (entity) {
2927
- resolvedEntity.fields[fieldKey] = entity;
2928
- }
2929
- }
2930
- else if (field && Array.isArray(field)) {
2931
- resolvedEntity.fields[fieldKey] = field.map((innerField) => {
2932
- if (innerField && innerField.sys?.type === 'Link') {
2933
- const entity = entityStore.getEntityFromLink(innerField);
2934
- if (entity) {
2935
- return entity;
2936
- }
2937
- }
2938
- return innerField;
2939
- });
2940
- }
2941
- });
2942
2968
  return resolvedEntity;
2943
2969
  }
2944
2970
 
2971
+ const excludeUndefined = (value) => {
2972
+ return value !== undefined;
2973
+ };
2945
2974
  function getArrayValue(entryOrAsset, path, entityStore) {
2975
+ // NOTE: Not sure if we need this if-statement,
2976
+ // as it is NOT possible to bind to Array variable an Asset
2977
+ // (as Assets don't have multi-reference fields) unless it's a degenerate case.
2946
2978
  if (entryOrAsset.sys.type === 'Asset') {
2947
2979
  return entryOrAsset;
2948
2980
  }
2949
- const arrayValue = get(entryOrAsset, path.split('/').slice(2, -1));
2981
+ const fieldName = path.split('/').slice(2, -1);
2982
+ const arrayValue = get(entryOrAsset, fieldName);
2950
2983
  if (!isArray(arrayValue)) {
2951
- console.warn(`Expected a value to be an array, but got: ${JSON.stringify(arrayValue)}`);
2984
+ console.warn(`A field '${fieldName}' of an entity was bound to an Array variable. Expected value of that field to be an array, but got: ${JSON.stringify(arrayValue)}`, { entity: entryOrAsset });
2952
2985
  return;
2953
2986
  }
2954
- const result = arrayValue.map((value) => {
2987
+ const result = arrayValue
2988
+ .map((value) => {
2955
2989
  if (typeof value === 'string') {
2956
- return value;
2990
+ return value; // handles case where Text array is bound (in [Content Model] tab of the platform, select Text and make it a list)
2957
2991
  }
2958
2992
  else if (value?.sys?.type === 'Link') {
2959
2993
  const resolvedEntity = entityStore.getEntityFromLink(value);
2960
2994
  if (!resolvedEntity) {
2995
+ // We return undefined, which means that entity wasn't availble in the Entity Store due to:
2996
+ // - because it's archived entity (and they normally wouldn't be sent to the Entity Store)
2997
+ // - bug where some entity wasn't added to the Entity Store
2998
+ // BTW, deleted entities shouldn't even be possible here as they require CT deletion first and that shouldn't allow us to load them at all)
2961
2999
  return;
2962
3000
  }
2963
- //resolve any embedded links - we currently only support 2 levels deep
2964
- const fields = resolvedEntity.fields || {};
2965
- Object.entries(fields).forEach(([fieldKey, field]) => {
2966
- if (field && field.sys?.type === 'Link') {
2967
- const entity = entityStore.getEntityFromLink(field);
2968
- if (entity) {
2969
- resolvedEntity.fields[fieldKey] = entity;
2970
- }
2971
- }
2972
- });
2973
3001
  return resolvedEntity;
2974
3002
  }
2975
3003
  else {
2976
3004
  console.warn(`Expected value to be a string or Link, but got: ${JSON.stringify(value)}`);
2977
3005
  return undefined;
2978
3006
  }
2979
- });
2980
- return result;
3007
+ })
3008
+ .filter(excludeUndefined);
3009
+ // eg. imagine you have multi-referene field with 3 links to archived entries,
3010
+ // all of them will be undefined on previous step and will be filtered out
3011
+ // of resultWithoutUndefined. Instead of passing to component an empty array,
3012
+ // we pass undefined. This means that develloper making custom component
3013
+ // does not have to handle empty array case. But only undefiened, which signals:
3014
+ // user didn't bind anything; user bound to reference field which is unset; all references are archived
3015
+ return result.length > 0 ? result : undefined;
2981
3016
  }
2982
3017
 
2983
3018
  const transformBoundContentValue = (variables, entityStore, binding, resolveDesignValue, variableName, variableType, path) => {
@@ -3038,16 +3073,6 @@ function treeMap(node, onNode) {
3038
3073
  return newNode;
3039
3074
  }
3040
3075
 
3041
- const isExperienceEntry = (entry) => {
3042
- return (entry?.sys?.type === 'Entry' &&
3043
- !!entry.fields?.title &&
3044
- !!entry.fields?.slug &&
3045
- !!entry.fields?.componentTree &&
3046
- Array.isArray(entry.fields.componentTree.breakpoints) &&
3047
- Array.isArray(entry.fields.componentTree.children) &&
3048
- typeof entry.fields.componentTree.schemaVersion === 'string');
3049
- };
3050
-
3051
3076
  const getDataFromTree = (tree) => {
3052
3077
  let dataSource = {};
3053
3078
  let unboundValues = {};
@@ -3122,8 +3147,6 @@ const checkIsAssemblyNode = ({ componentId, usedComponents, }) => {
3122
3147
  return false;
3123
3148
  return usedComponents.some((usedComponent) => usedComponent.sys.id === componentId);
3124
3149
  };
3125
- /** @deprecated use `checkIsAssemblyNode` instead. Will be removed with SDK v5. */
3126
- const checkIsAssembly = checkIsAssemblyNode;
3127
3150
  /**
3128
3151
  * This check assumes that the entry is already ensured to be an experience, i.e. the
3129
3152
  * content type of the entry is an experience type with the necessary annotations.
@@ -3221,6 +3244,124 @@ const validateExperienceBuilderConfig = ({ locale, mode, }) => {
3221
3244
  }
3222
3245
  };
3223
3246
 
3247
+ const uniqueById = (arr) => {
3248
+ const map = new Map();
3249
+ arr.forEach((item) => map.set(item.sys.id, item));
3250
+ return [...map.values()];
3251
+ };
3252
+ const isObject = (value) => {
3253
+ return typeof value === 'object' && value !== null;
3254
+ };
3255
+ /**
3256
+ * Extracts all references from an entry.
3257
+ * Handles both: reference and multi-reference fields.
3258
+ * Returns unique array of references (even if they repeat within the entry).
3259
+ */
3260
+ const referencesOf = (entry, fnShouldFollowReferencesOfEntryField) => {
3261
+ const references = [];
3262
+ const handleArray = (fieldValue, _fieldName) => {
3263
+ for (const item of fieldValue) {
3264
+ if (isObject(item) && item.sys?.type === 'Link') {
3265
+ references.push(item);
3266
+ }
3267
+ }
3268
+ };
3269
+ const handleLink = (fieldValue, _fieldName) => {
3270
+ references.push(fieldValue);
3271
+ };
3272
+ for (const [fieldName, fieldValue] of Object.entries(entry.fields)) {
3273
+ if (fnShouldFollowReferencesOfEntryField &&
3274
+ !fnShouldFollowReferencesOfEntryField(fieldName, entry)) {
3275
+ continue;
3276
+ }
3277
+ if (fieldValue === undefined) {
3278
+ continue; // edge case when field is present on object, but is set to undefined explicitly e.g. during test mocking { myField: undefined }
3279
+ }
3280
+ if (Array.isArray(fieldValue)) {
3281
+ handleArray(fieldValue);
3282
+ }
3283
+ else if (fieldValue !== null &&
3284
+ isObject(fieldValue) &&
3285
+ fieldValue.sys?.type === 'Link') {
3286
+ handleLink(fieldValue);
3287
+ }
3288
+ }
3289
+ return uniqueById(references);
3290
+ };
3291
+ // -- REFERENCE EXTRACTION UTILITIES --
3292
+ function extractReferencesFromEntriesAsIds(entries) {
3293
+ const [uniqueEntries, uniqueAssets, uniqueReferences] = extractReferencesFromEntries(entries);
3294
+ const entryIds = uniqueEntries.map((link) => link.sys.id);
3295
+ const assetIds = uniqueAssets.map((link) => link.sys.id);
3296
+ const referenceIds = uniqueReferences.map((link) => link.sys.id);
3297
+ return [entryIds, assetIds, referenceIds];
3298
+ }
3299
+ function extractReferencesFromEntries(entries) {
3300
+ const allReferences = entries.flatMap((entry) => referencesOf(entry));
3301
+ const uniqueReferences = uniqueById(allReferences);
3302
+ const uniqueAssets = uniqueReferences.filter((link) => link.sys.linkType === 'Asset');
3303
+ const uniqueEntries = uniqueReferences.filter((link) => link.sys.linkType === 'Entry');
3304
+ return [uniqueEntries, uniqueAssets, uniqueReferences];
3305
+ }
3306
+
3307
+ const excludeAssets = (entity) => !isAsset(entity);
3308
+ const excludePatternEntries = (entry) => !isPatternEntry(entry);
3309
+ /**
3310
+ * Parses experience and extracts all leaf links that are referenced from the experience.
3311
+ * PRECONDITION: Relies on the fact that entityStore is preloaded with all dataSource
3312
+ * entries using include=2 (meaning that up to L3 entries are already preloaded into EntitStore).
3313
+ *
3314
+ * The function iterates over all entries in the entityStore (assuming they can be L1, L2, L3) and
3315
+ * over all of their references. Any references that are NOT available in the entityStore are considered
3316
+ * "leaf references" aka "leaf links" and are returned.
3317
+ *
3318
+ * The EntityStore happens to contain also entities representing patterns, which we do NOT consider
3319
+ * as entries that point to leaf links. So we don't iterate over patterns only over entries which
3320
+ * can be used for binding.
3321
+ */
3322
+ const extractLeafLinksReferencedFromExperience = (experience) => {
3323
+ const assetLinks = [];
3324
+ const entryLinks = [];
3325
+ if (!experience.entityStore) {
3326
+ throw new Error('Parameter `experience` should have valid `experience.entityStore` object. Without it, we cannot extract leaf links. Most likely you passed `experience` instance that was not fully fetched. Check your experience fetching logic.');
3327
+ }
3328
+ // We want only leaf links which can be used for binding. We use two filters:
3329
+ // excludeAssets: because assets do not have references, so we don't need to traverse them
3330
+ // excludePatternEntries: because EntityStore happens to also store pattern-entries.
3331
+ // Those point to other patterns, and we don't want to consider them as
3332
+ // parents of leaf links pointing to actual data carrying entries used for binding.
3333
+ const entries = experience.entityStore.entities
3334
+ .filter(excludeAssets)
3335
+ .filter(excludePatternEntries);
3336
+ // We assume that ALL of the entries in the experience
3337
+ for (const entry of entries) {
3338
+ const references = referencesOf(entry);
3339
+ for (const ref of references) {
3340
+ if (isLinkToAsset(ref)) {
3341
+ if (!experience.entityStore.getEntityFromLink(ref)) {
3342
+ assetLinks.push(ref);
3343
+ }
3344
+ }
3345
+ else if (isLinkToEntry(ref)) {
3346
+ if (!experience.entityStore.getEntityFromLink(ref)) {
3347
+ entryLinks.push(ref);
3348
+ }
3349
+ }
3350
+ else {
3351
+ console.warn(`Unexpected reference type found in entry "${entry.sys.id}": ${JSON.stringify(ref)}`);
3352
+ }
3353
+ }
3354
+ }
3355
+ const dedupedAssetLinks = uniqueById(assetLinks);
3356
+ const dedupedEntryLinks = uniqueById(entryLinks);
3357
+ return {
3358
+ assetLinks: dedupedAssetLinks,
3359
+ entryLinks: dedupedEntryLinks,
3360
+ assetIds: dedupedAssetLinks.map((link) => link.sys.id),
3361
+ entryIds: dedupedEntryLinks.map((link) => link.sys.id),
3362
+ };
3363
+ };
3364
+
3224
3365
  const sendMessage = (eventType, data) => {
3225
3366
  if (typeof window === 'undefined') {
3226
3367
  return;
@@ -3237,6 +3378,17 @@ const sendMessage = (eventType, data) => {
3237
3378
  }, '*');
3238
3379
  };
3239
3380
 
3381
+ function deepFreeze(obj) {
3382
+ const propNames = Object.getOwnPropertyNames(obj);
3383
+ for (const name of propNames) {
3384
+ const value = obj[name];
3385
+ if (value && typeof value === 'object') {
3386
+ deepFreeze(value);
3387
+ }
3388
+ }
3389
+ return Object.freeze(obj);
3390
+ }
3391
+
3240
3392
  /**
3241
3393
  * Base Store for entities
3242
3394
  * Can be extended for the different loading behaviours (editor, production, ..)
@@ -3305,6 +3457,22 @@ class EntityStoreBase {
3305
3457
  }
3306
3458
  return resolvedEntity;
3307
3459
  }
3460
+ getAssetById(assetId) {
3461
+ const asset = this.assetMap.get(assetId);
3462
+ if (!asset) {
3463
+ console.warn(`Asset with ID "${assetId}" is not found in the store`);
3464
+ return;
3465
+ }
3466
+ return asset;
3467
+ }
3468
+ getEntryById(entryId) {
3469
+ const entry = this.entryMap.get(entryId);
3470
+ if (!entry) {
3471
+ console.warn(`Entry with ID "${entryId}" is not found in the store`);
3472
+ return;
3473
+ }
3474
+ return entry;
3475
+ }
3308
3476
  getEntitiesFromMap(type, ids) {
3309
3477
  const resolved = [];
3310
3478
  const missing = [];
@@ -3324,10 +3492,12 @@ class EntityStoreBase {
3324
3492
  }
3325
3493
  addEntity(entity) {
3326
3494
  if (isAsset(entity)) {
3327
- this.assetMap.set(entity.sys.id, entity);
3495
+ // cloned and frozen
3496
+ this.assetMap.set(entity.sys.id, deepFreeze(structuredClone(entity)));
3328
3497
  }
3329
3498
  else if (isEntry(entity)) {
3330
- this.entryMap.set(entity.sys.id, entity);
3499
+ // cloned and frozen
3500
+ this.entryMap.set(entity.sys.id, deepFreeze(structuredClone(entity)));
3331
3501
  }
3332
3502
  else {
3333
3503
  throw new Error(`Attempted to add an entity to the store that is neither Asset nor Entry: '${JSON.stringify(entity)}'`);
@@ -3592,7 +3762,6 @@ class EditorModeEntityStore extends EditorEntityStore {
3592
3762
  };
3593
3763
  };
3594
3764
  super({ entities, sendMessage, subscribe, locale, timeoutDuration: REQUEST_TIMEOUT });
3595
- this.locale = locale;
3596
3765
  }
3597
3766
  /**
3598
3767
  * This function collects and returns the list of requested entries and assets. Additionally, it checks
@@ -3820,6 +3989,89 @@ class EntityStore extends EntityStoreBase {
3820
3989
  }
3821
3990
  }
3822
3991
 
3992
+ class UninitializedEntityStore extends EntityStoreBase {
3993
+ constructor() {
3994
+ super({ entities: [], locale: 'uninitialized-locale-in-uninitialized-entity-store' });
3995
+ }
3996
+ }
3997
+
3998
+ const inMemoryEntitiesStore = create((set, get) => ({
3999
+ // The UninitializedEntityStore is a placeholder instance and is here to highlight the
4000
+ // // fact that it's not used by anything until during loading lifecycle it'sreplaced by real entity store:
4001
+ // - in Preview+Delivery mode: right after we fetch Expereince and it entities
4002
+ // - in EDITOR (VisualEditor) mode: right after the VisualEditor is async imported and initialize event happens
4003
+ entityStore: new UninitializedEntityStore(),
4004
+ areEntitiesFetched: false,
4005
+ setEntitiesFetched(fetched) {
4006
+ set({ areEntitiesFetched: fetched });
4007
+ },
4008
+ resolveAssetById(assetId) {
4009
+ if (!assetId)
4010
+ return undefined;
4011
+ const { entityStore } = get();
4012
+ return entityStore.getAssetById(assetId);
4013
+ },
4014
+ resolveEntryById(entryId) {
4015
+ if (!entryId)
4016
+ return undefined;
4017
+ const { entityStore } = get();
4018
+ return entityStore.getEntryById(entryId);
4019
+ },
4020
+ resolveEntity(link) {
4021
+ if (!link)
4022
+ return undefined;
4023
+ const { entityStore } = get();
4024
+ return entityStore.getEntityFromLink(link);
4025
+ },
4026
+ resetEntityStore(entityStore) {
4027
+ set({
4028
+ entityStore,
4029
+ areEntitiesFetched: false,
4030
+ });
4031
+ },
4032
+ }));
4033
+
4034
+ function maybeResolveLink(maybeLink) {
4035
+ if (!isLink(maybeLink)) {
4036
+ console.warn('maybeResolveLink function must receive Link shape. Provided argument does not match the Link shape: ', maybeLink);
4037
+ return undefined;
4038
+ }
4039
+ return inMemoryEntitiesStore.getState().resolveEntity(maybeLink);
4040
+ }
4041
+ function maybeResolveByAssetId(assetId) {
4042
+ return inMemoryEntitiesStore.getState().resolveAssetById(assetId);
4043
+ }
4044
+ function maybeResolveByEntryId(entryId) {
4045
+ return inMemoryEntitiesStore.getState().resolveEntryById(entryId);
4046
+ }
4047
+ function hasEntry(entryId) {
4048
+ return Boolean(maybeResolveByEntryId(entryId));
4049
+ }
4050
+ function hasAsset(assetId) {
4051
+ return Boolean(maybeResolveByAssetId(assetId));
4052
+ }
4053
+ function addEntities(entities) {
4054
+ if (!Array.isArray(entities) || entities.length === 0) {
4055
+ return;
4056
+ }
4057
+ const { entityStore } = inMemoryEntitiesStore.getState();
4058
+ const definedEntities = entities.filter(Boolean);
4059
+ for (const entity of definedEntities) {
4060
+ entityStore.updateEntity(entity);
4061
+ }
4062
+ }
4063
+ const inMemoryEntities = {
4064
+ maybeResolveLink,
4065
+ maybeResolveByAssetId,
4066
+ maybeResolveByEntryId,
4067
+ hasEntry,
4068
+ hasAsset,
4069
+ addEntities,
4070
+ };
4071
+ const useInMemoryEntities = () => {
4072
+ return inMemoryEntities;
4073
+ };
4074
+
3823
4075
  var VisualEditorMode;
3824
4076
  (function (VisualEditorMode) {
3825
4077
  VisualEditorMode["LazyLoad"] = "lazyLoad";
@@ -3827,11 +4079,9 @@ var VisualEditorMode;
3827
4079
  })(VisualEditorMode || (VisualEditorMode = {}));
3828
4080
 
3829
4081
  function createExperience(options) {
4082
+ let entityStore;
3830
4083
  if (typeof options === 'string') {
3831
- const entityStore = new EntityStore(options);
3832
- return {
3833
- entityStore,
3834
- };
4084
+ entityStore = new EntityStore(options);
3835
4085
  }
3836
4086
  else {
3837
4087
  const { experienceEntry, referencedAssets, referencedEntries, locale } = options;
@@ -3841,15 +4091,16 @@ function createExperience(options) {
3841
4091
  if (!isExperienceEntry(experienceEntry)) {
3842
4092
  throw new Error('Provided entry is not an experience entry');
3843
4093
  }
3844
- const entityStore = new EntityStore({
4094
+ entityStore = new EntityStore({
3845
4095
  experienceEntry,
3846
4096
  entities: [...referencedEntries, ...referencedAssets],
3847
4097
  locale,
3848
4098
  });
3849
- return {
3850
- entityStore,
3851
- };
3852
4099
  }
4100
+ inMemoryEntitiesStore.getState().resetEntityStore(entityStore);
4101
+ return {
4102
+ entityStore,
4103
+ };
3853
4104
  }
3854
4105
  // Following the API shape, we check the `sys.locale` property as we can't rely on the shape of
3855
4106
  // fields to determine whether it's localized or not.
@@ -4443,5 +4694,5 @@ async function fetchById({ client, experienceTypeId, id, localeCode, isEditorMod
4443
4694
  }
4444
4695
  }
4445
4696
 
4446
- export { DebugLogger, DeepReference, EditorModeEntityStore, EntityStore, EntityStoreBase, MEDIA_QUERY_REGEXP, VisualEditorMode, addLocale, addMinHeightForEmptyStructures, breakpointsRegistry, buildCfStyles, buildStyleTag, buildTemplate, builtInStyles, calculateNodeDefaultHeight, checkIsAssembly, checkIsAssemblyDefinition, checkIsAssemblyEntry, checkIsAssemblyNode, columnsBuiltInStyles, containerBuiltInStyles, createExperience, debug, defineBreakpoints, defineDesignTokens, designTokensRegistry, detachExperienceStyles, disableDebug, dividerBuiltInStyles, doesMismatchMessageSchema, enableDebug, fetchAllAssets, fetchAllEntries, fetchById, fetchBySlug, fetchExperienceEntry, fetchReferencedEntities, findOutermostCoordinates, flattenDesignTokenRegistry, gatherDeepReferencesFromExperienceEntry, gatherDeepReferencesFromTree, generateRandomId, getActiveBreakpointIndex, getBreakpointRegistration, getDataFromTree, getDesignTokenRegistration, getElementCoordinates, getFallbackBreakpointIndex, getInsertionData, getTargetValueInPixels, getTemplateValue, getValueForBreakpoint, indexByBreakpoint, isAsset, isCfStyleAttribute, isComponentAllowedOnRoot, isContentfulComponent, isContentfulStructureComponent, isDeepPath, isEntry, isExperienceEntry, isLink, isLinkToAsset, isPatternComponent, isStructureWithRelativeHeight, isValidBreakpointValue, lastPathNamedSegmentEq, localizeEntity, maybePopulateDesignTokenValue, mediaQueryMatcher, optionalBuiltInStyles, parseCSSValue, parseDataSourcePathIntoFieldset, parseDataSourcePathWithL1DeepBindings, resetBreakpointsRegistry, resetDesignTokenRegistry, resolveBackgroundImageBinding, resolveHyperlinkPattern, runBreakpointsValidation, sanitizeNodeProps, sectionBuiltInStyles, sendMessage, singleColumnBuiltInStyles, stringifyCssProperties, toCSSAttribute, toMediaQuery, transformBoundContentValue, transformVisibility, treeMap, treeVisit, tryParseMessage, validateExperienceBuilderConfig };
4697
+ export { DebugLogger, DeepReference, EditorModeEntityStore, EntityStore, EntityStoreBase, MEDIA_QUERY_REGEXP, VisualEditorMode, addLocale, addMinHeightForEmptyStructures, breakpointsRegistry, buildCfStyles, buildStyleTag, buildTemplate, builtInStyles, calculateNodeDefaultHeight, checkIsAssemblyDefinition, checkIsAssemblyEntry, checkIsAssemblyNode, columnsBuiltInStyles, containerBuiltInStyles, createExperience, debug, defineBreakpoints, defineDesignTokens, designTokensRegistry, detachExperienceStyles, disableDebug, dividerBuiltInStyles, doesMismatchMessageSchema, enableDebug, extractLeafLinksReferencedFromExperience, extractReferencesFromEntries, extractReferencesFromEntriesAsIds, fetchAllAssets, fetchAllEntries, fetchById, fetchBySlug, fetchExperienceEntry, fetchReferencedEntities, findOutermostCoordinates, flattenDesignTokenRegistry, gatherDeepReferencesFromExperienceEntry, gatherDeepReferencesFromTree, generateRandomId, getActiveBreakpointIndex, getBreakpointRegistration, getDataFromTree, getDesignTokenRegistration, getElementCoordinates, getFallbackBreakpointIndex, getInsertionData, getTargetValueInPixels, getTemplateValue, getValueForBreakpoint, inMemoryEntities, inMemoryEntitiesStore, indexByBreakpoint, isArrayOfLinks, isAsset, isCfStyleAttribute, isComponentAllowedOnRoot, isContentfulComponent, isContentfulStructureComponent, isDeepPath, isEntry, isExperienceEntry, isLink, isLinkToAsset, isLinkToEntry, isPatternComponent, isPatternEntry, isStructureWithRelativeHeight, isValidBreakpointValue, lastPathNamedSegmentEq, localizeEntity, maybePopulateDesignTokenValue, mediaQueryMatcher, mergeDesignValuesByBreakpoint, optionalBuiltInStyles, parseCSSValue, parseDataSourcePathIntoFieldset, parseDataSourcePathWithL1DeepBindings, referencesOf, resetBreakpointsRegistry, resetDesignTokenRegistry, resolveBackgroundImageBinding, resolveHyperlinkPattern, runBreakpointsValidation, sanitizeNodeProps, sectionBuiltInStyles, sendMessage, singleColumnBuiltInStyles, stringifyCssProperties, toCSSAttribute, toMediaQuery, transformBoundContentValue, transformVisibility, treeMap, treeVisit, tryParseMessage, uniqueById, useInMemoryEntities, validateExperienceBuilderConfig };
4447
4698
  //# sourceMappingURL=index.js.map