@contentful/experiences-core 1.36.0-dev-20250417T0749-6e3b573.0 → 1.36.0-dev-20250417T1301-b6204ec.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.
@@ -12,7 +12,7 @@ declare abstract class EntityStoreBase {
12
12
  entities: Array<Entry | Asset>;
13
13
  locale: string;
14
14
  });
15
- get entities(): (Entry | Asset<ChainModifiers, string>)[];
15
+ get entities(): (Asset<ChainModifiers, string> | Entry)[];
16
16
  updateEntity(entity: Entry | Asset): void;
17
17
  getEntryOrAsset(linkOrEntryOrAsset: UnresolvedLink<'Entry' | 'Asset'> | Asset | Entry, path: string): Entry | Asset | undefined;
18
18
  /**
@@ -24,7 +24,7 @@ declare abstract class EntityStoreBase {
24
24
  getValue(entityLink: UnresolvedLink<'Entry' | 'Asset'>, path: string[]): string | undefined;
25
25
  getEntityFromLink(link: UnresolvedLink<'Entry' | 'Asset'>): Asset | Entry | undefined;
26
26
  protected getEntitiesFromMap(type: 'Entry' | 'Asset', ids: string[]): {
27
- resolved: (Entry | Asset<ChainModifiers, string>)[];
27
+ resolved: (Asset<ChainModifiers, string> | Entry)[];
28
28
  missing: string[];
29
29
  };
30
30
  protected addEntity(entity: Entry | Asset): void;
package/dist/index.js CHANGED
@@ -1645,6 +1645,69 @@ const resetBreakpointsRegistry = () => {
1645
1645
  breakpointsRegistry = [];
1646
1646
  };
1647
1647
 
1648
+ function getOptimizedImageUrl(url, width, quality, format) {
1649
+ if (url.startsWith('//')) {
1650
+ url = 'https:' + url;
1651
+ }
1652
+ const params = new URLSearchParams();
1653
+ if (width) {
1654
+ params.append('w', width.toString());
1655
+ }
1656
+ if (quality && quality > 0 && quality < 100) {
1657
+ params.append('q', quality.toString());
1658
+ }
1659
+ if (format) {
1660
+ params.append('fm', format);
1661
+ }
1662
+ const queryString = params.toString();
1663
+ return `${url}${queryString ? '?' + queryString : ''}`;
1664
+ }
1665
+
1666
+ function validateParams(file, quality, format) {
1667
+ if (!file.details.image) {
1668
+ throw Error('No image in file asset to transform');
1669
+ }
1670
+ if (quality < 0 || quality > 100) {
1671
+ throw Error('Quality must be between 0 and 100');
1672
+ }
1673
+ if (format && !SUPPORTED_IMAGE_FORMATS.includes(format)) {
1674
+ throw Error(`Format must be one of ${SUPPORTED_IMAGE_FORMATS.join(', ')}`);
1675
+ }
1676
+ return true;
1677
+ }
1678
+
1679
+ const MAX_WIDTH_ALLOWED$1 = 2000;
1680
+ const getOptimizedBackgroundImageAsset = (file, widthStyle, quality = '100%', format) => {
1681
+ const qualityNumber = Number(quality.replace('%', ''));
1682
+ if (!validateParams(file, qualityNumber, format)) ;
1683
+ if (!validateParams(file, qualityNumber, format)) ;
1684
+ const url = file.url;
1685
+ const { width1x, width2x } = getWidths(widthStyle, file);
1686
+ const imageUrl1x = getOptimizedImageUrl(url, width1x, qualityNumber, format);
1687
+ const imageUrl2x = getOptimizedImageUrl(url, width2x, qualityNumber, format);
1688
+ const srcSet = [`url(${imageUrl1x}) 1x`, `url(${imageUrl2x}) 2x`];
1689
+ const returnedUrl = getOptimizedImageUrl(url, width2x, qualityNumber, format);
1690
+ const optimizedBackgroundImageAsset = {
1691
+ url: returnedUrl,
1692
+ srcSet,
1693
+ file,
1694
+ };
1695
+ return optimizedBackgroundImageAsset;
1696
+ function getWidths(widthStyle, file) {
1697
+ let width1x = 0;
1698
+ let width2x = 0;
1699
+ const intrinsicImageWidth = file.details.image.width;
1700
+ if (widthStyle.endsWith('px')) {
1701
+ width1x = Math.min(Number(widthStyle.replace('px', '')), intrinsicImageWidth);
1702
+ }
1703
+ else {
1704
+ width1x = Math.min(MAX_WIDTH_ALLOWED$1, intrinsicImageWidth);
1705
+ }
1706
+ width2x = Math.min(width1x * 2, intrinsicImageWidth);
1707
+ return { width1x, width2x };
1708
+ }
1709
+ };
1710
+
1648
1711
  const detachExperienceStyles = (experience) => {
1649
1712
  const experienceTreeRoot = experience.entityStore?.experienceEntryFields
1650
1713
  ?.componentTree;
@@ -1664,17 +1727,25 @@ const detachExperienceStyles = (experience) => {
1664
1727
  }), {});
1665
1728
  // getting the breakpoint ids
1666
1729
  const breakpointIds = Object.keys(mediaQueryDataByBreakpoint);
1667
- const iterateOverTreeAndExtractStyles = ({ componentTree, dataSource, unboundValues, componentSettings, componentVariablesOverwrites, patternWrapper, wrappingPatternIds, patternNodeIdsChain = '', }) => {
1730
+ const iterateOverTreeAndExtractStyles = ({ componentTree, dataSource, unboundValues, componentSettings, componentVariablesOverwrites, patternWrapper, wrappingPatternIds, parentChainArr = [], }) => {
1668
1731
  // traversing the tree
1669
1732
  const queue = [];
1670
- queue.push(...componentTree.children);
1671
- let currentNode = undefined;
1733
+ queue.push(...componentTree.children.map((child) => ({
1734
+ node: child,
1735
+ parentChain: [...parentChainArr],
1736
+ })));
1672
1737
  // for each tree node
1673
1738
  while (queue.length) {
1674
- currentNode = queue.shift();
1739
+ const queueItem = queue.shift();
1740
+ if (!queueItem) {
1741
+ break;
1742
+ }
1743
+ const { node: currentNode, parentChain } = queueItem;
1675
1744
  if (!currentNode) {
1676
1745
  break;
1677
1746
  }
1747
+ const currentNodeParentChain = [...parentChain, currentNode.id || ''];
1748
+ const currentPatternNodeIdsChain = currentNodeParentChain.join('');
1678
1749
  const usedComponents = experience.entityStore?.usedComponents ?? [];
1679
1750
  const isPatternNode = checkIsAssemblyNode({
1680
1751
  componentId: currentNode.definitionId,
@@ -1712,7 +1783,7 @@ const detachExperienceStyles = (experience) => {
1712
1783
  // pass top-level pattern node to store instance-specific child styles for rendering
1713
1784
  patternWrapper: currentNode,
1714
1785
  wrappingPatternIds: new Set([...wrappingPatternIds, currentNode.definitionId]),
1715
- patternNodeIdsChain: `${patternNodeIdsChain}${currentNode.id}`,
1786
+ parentChainArr: currentNodeParentChain,
1716
1787
  });
1717
1788
  continue;
1718
1789
  }
@@ -1798,13 +1869,12 @@ const detachExperienceStyles = (experience) => {
1798
1869
  // making sure that we respect the order of breakpoints from
1799
1870
  // we can achieve "desktop first" or "mobile first" approach to style over-writes
1800
1871
  if (patternWrapper) {
1801
- currentNode.id = currentNode.id || generateRandomId(5);
1802
1872
  // @ts-expect-error -- valueByBreakpoint is not explicitly defined, but it's already defined in the patternWrapper styles
1803
1873
  patternWrapper.variables.cfSsrClassName = {
1804
1874
  ...(patternWrapper.variables.cfSsrClassName ?? {}),
1805
1875
  type: 'DesignValue',
1806
1876
  // Chain IDs to avoid overwriting styles across multiple instances of the same pattern
1807
- [`${patternNodeIdsChain}${currentNode.id}`]: {
1877
+ [currentPatternNodeIdsChain]: {
1808
1878
  valuesByBreakpoint: {
1809
1879
  [breakpointIds[0]]: currentNodeClassNames.join(' '),
1810
1880
  },
@@ -1819,7 +1889,10 @@ const detachExperienceStyles = (experience) => {
1819
1889
  },
1820
1890
  };
1821
1891
  }
1822
- queue.push(...currentNode.children);
1892
+ queue.push(...currentNode.children.map((child) => ({
1893
+ node: child,
1894
+ parentChain: currentNodeParentChain,
1895
+ })));
1823
1896
  }
1824
1897
  };
1825
1898
  iterateOverTreeAndExtractStyles({
@@ -1941,7 +2014,28 @@ const maybePopulateDesignTokenValue = (variableName, variableValue, mapOfDesignV
1941
2014
  // Not trimming would end up with a trailing space that breaks the check in `calculateNodeDefaultHeight`
1942
2015
  return resolvedValue.trim();
1943
2016
  };
1944
- const resolveBackgroundImageBinding = ({ variableData, getBoundEntityById, dataSource = {}, unboundValues = {}, componentVariablesOverwrites, componentSettings = { variableDefinitions: {} }, }) => {
2017
+ const transformMedia$1 = (boundAsset, width, options) => {
2018
+ try {
2019
+ const asset = boundAsset;
2020
+ // Target width (px/rem/em) will be applied to the css url if it's lower than the original image width (in px)
2021
+ const assetDetails = asset.fields.file?.details;
2022
+ const assetWidth = assetDetails?.image?.width || 0; // This is always in px
2023
+ if (!options) {
2024
+ return asset.fields.file?.url;
2025
+ }
2026
+ const targetWidthObject = parseCSSValue(options.targetSize); // Contains value and unit (px/rem/em) so convert and then compare to assetWidth
2027
+ const targetValue = targetWidthObject ? getTargetValueInPixels(targetWidthObject) : assetWidth;
2028
+ if (targetValue < assetWidth)
2029
+ width = `${targetValue}px`;
2030
+ const value = getOptimizedBackgroundImageAsset(asset.fields.file, width, options.quality, options.format);
2031
+ return value;
2032
+ }
2033
+ catch (error) {
2034
+ console.error('Error transforming image asset', error);
2035
+ }
2036
+ return boundAsset.fields.file?.url;
2037
+ };
2038
+ const resolveBackgroundImageBinding = ({ variableData, getBoundEntityById, dataSource = {}, unboundValues = {}, componentVariablesOverwrites, componentSettings = { variableDefinitions: {} }, options, width, }) => {
1945
2039
  if (variableData.type === 'UnboundValue') {
1946
2040
  const uuid = variableData.key;
1947
2041
  return unboundValues[uuid]?.value;
@@ -1966,6 +2060,8 @@ const resolveBackgroundImageBinding = ({ variableData, getBoundEntityById, dataS
1966
2060
  unboundValues,
1967
2061
  componentVariablesOverwrites,
1968
2062
  componentSettings,
2063
+ options,
2064
+ width,
1969
2065
  });
1970
2066
  return resolvedValue || defaultValue;
1971
2067
  }
@@ -1978,7 +2074,7 @@ const resolveBackgroundImageBinding = ({ variableData, getBoundEntityById, dataS
1978
2074
  return;
1979
2075
  }
1980
2076
  if (boundEntity.sys.type === 'Asset') {
1981
- return boundEntity.fields.file?.url;
2077
+ return transformMedia$1(boundEntity, width, options);
1982
2078
  }
1983
2079
  else {
1984
2080
  // '/lUERH7tX7nJTaPX6f0udB/fields/assetReference/~locale/fields/file/~locale'
@@ -2002,11 +2098,31 @@ const resolveBackgroundImageBinding = ({ variableData, getBoundEntityById, dataS
2002
2098
  if (!referencedAsset) {
2003
2099
  return;
2004
2100
  }
2005
- return referencedAsset.fields.file?.url;
2101
+ return transformMedia$1(referencedAsset, width, options);
2006
2102
  }
2007
2103
  }
2008
2104
  }
2009
2105
  };
2106
+ const resolveVariable = ({ variableData, defaultBreakpoint, componentSettings = { variableDefinitions: {} }, componentVariablesOverwrites, }) => {
2107
+ if (variableData?.type === 'DesignValue') {
2108
+ return variableData.valuesByBreakpoint[defaultBreakpoint] || {};
2109
+ }
2110
+ else if (variableData?.type === 'ComponentValue') {
2111
+ const variableDefinitionKey = variableData.key;
2112
+ const variableDefinition = componentSettings.variableDefinitions[variableDefinitionKey];
2113
+ const defaultValue = variableDefinition.defaultValue;
2114
+ const userSetValue = componentVariablesOverwrites?.[variableDefinitionKey];
2115
+ if (!userSetValue || userSetValue.type === 'ComponentValue') {
2116
+ return defaultValue?.valuesByBreakpoint[defaultBreakpoint] || '';
2117
+ }
2118
+ return resolveVariable({
2119
+ variableData: userSetValue,
2120
+ defaultBreakpoint,
2121
+ componentSettings,
2122
+ componentVariablesOverwrites,
2123
+ });
2124
+ }
2125
+ };
2010
2126
  /**
2011
2127
  * Takes the initial set of properties, filters only design properties that will be mapped to CSS and
2012
2128
  * re-organizes them to be indexed by breakpoint ID ("breakpoint > variable > value"). It will
@@ -2057,6 +2173,22 @@ const indexByBreakpoint = ({ variables, breakpointIds, getBoundEntityById, unbou
2057
2173
  if (variableName === 'cfBackgroundImageUrl' ||
2058
2174
  // TODO: Test this for nested patterns as the name might be just a random hash without the actual name (needs to be validated).
2059
2175
  variableName.startsWith('cfBackgroundImageUrl_')) {
2176
+ const width = resolveVariable({
2177
+ variableData: variables['cfWidth'],
2178
+ defaultBreakpoint,
2179
+ componentSettings,
2180
+ componentVariablesOverwrites,
2181
+ });
2182
+ const options = resolveVariable({
2183
+ variableData: variables['cfBackgroundImageOptions'],
2184
+ defaultBreakpoint,
2185
+ componentSettings,
2186
+ componentVariablesOverwrites,
2187
+ });
2188
+ if (!options) {
2189
+ console.error(`Error transforming image asset: Required variable [cfBackgroundImageOptions] missing from component definition`);
2190
+ continue;
2191
+ }
2060
2192
  const imageUrl = resolveBackgroundImageBinding({
2061
2193
  variableData,
2062
2194
  getBoundEntityById,
@@ -2064,6 +2196,8 @@ const indexByBreakpoint = ({ variables, breakpointIds, getBoundEntityById, unbou
2064
2196
  dataSource,
2065
2197
  componentSettings,
2066
2198
  componentVariablesOverwrites,
2199
+ width,
2200
+ options,
2067
2201
  });
2068
2202
  if (imageUrl) {
2069
2203
  variableValuesByBreakpoints[defaultBreakpoint][variableName] = imageUrl;
@@ -2203,69 +2337,6 @@ const resolveLinks = (node, entityStore) => {
2203
2337
  }
2204
2338
  };
2205
2339
 
2206
- function getOptimizedImageUrl(url, width, quality, format) {
2207
- if (url.startsWith('//')) {
2208
- url = 'https:' + url;
2209
- }
2210
- const params = new URLSearchParams();
2211
- if (width) {
2212
- params.append('w', width.toString());
2213
- }
2214
- if (quality && quality > 0 && quality < 100) {
2215
- params.append('q', quality.toString());
2216
- }
2217
- if (format) {
2218
- params.append('fm', format);
2219
- }
2220
- const queryString = params.toString();
2221
- return `${url}${queryString ? '?' + queryString : ''}`;
2222
- }
2223
-
2224
- function validateParams(file, quality, format) {
2225
- if (!file.details.image) {
2226
- throw Error('No image in file asset to transform');
2227
- }
2228
- if (quality < 0 || quality > 100) {
2229
- throw Error('Quality must be between 0 and 100');
2230
- }
2231
- if (format && !SUPPORTED_IMAGE_FORMATS.includes(format)) {
2232
- throw Error(`Format must be one of ${SUPPORTED_IMAGE_FORMATS.join(', ')}`);
2233
- }
2234
- return true;
2235
- }
2236
-
2237
- const MAX_WIDTH_ALLOWED$1 = 2000;
2238
- const getOptimizedBackgroundImageAsset = (file, widthStyle, quality = '100%', format) => {
2239
- const qualityNumber = Number(quality.replace('%', ''));
2240
- if (!validateParams(file, qualityNumber, format)) ;
2241
- if (!validateParams(file, qualityNumber, format)) ;
2242
- const url = file.url;
2243
- const { width1x, width2x } = getWidths(widthStyle, file);
2244
- const imageUrl1x = getOptimizedImageUrl(url, width1x, qualityNumber, format);
2245
- const imageUrl2x = getOptimizedImageUrl(url, width2x, qualityNumber, format);
2246
- const srcSet = [`url(${imageUrl1x}) 1x`, `url(${imageUrl2x}) 2x`];
2247
- const returnedUrl = getOptimizedImageUrl(url, width2x, qualityNumber, format);
2248
- const optimizedBackgroundImageAsset = {
2249
- url: returnedUrl,
2250
- srcSet,
2251
- file,
2252
- };
2253
- return optimizedBackgroundImageAsset;
2254
- function getWidths(widthStyle, file) {
2255
- let width1x = 0;
2256
- let width2x = 0;
2257
- const intrinsicImageWidth = file.details.image.width;
2258
- if (widthStyle.endsWith('px')) {
2259
- width1x = Math.min(Number(widthStyle.replace('px', '')), intrinsicImageWidth);
2260
- }
2261
- else {
2262
- width1x = Math.min(MAX_WIDTH_ALLOWED$1, intrinsicImageWidth);
2263
- }
2264
- width2x = Math.min(width1x * 2, intrinsicImageWidth);
2265
- return { width1x, width2x };
2266
- }
2267
- };
2268
-
2269
2340
  const MAX_WIDTH_ALLOWED = 4000;
2270
2341
  const getOptimizedImageAsset = ({ file, sizes, loading, quality = '100%', format, }) => {
2271
2342
  const qualityNumber = Number(quality.replace('%', ''));