Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/non-string-styles-params.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sitecore-content-sdk/content': patch
---

[content] Fix build crash and normalize DetailedRenderingParams object/JSON values for `Styles`, `CSSStyles`, `LibraryId`, and `GridParameters` rendering params
3 changes: 3 additions & 0 deletions packages/content/api/content-sdk-content.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,9 @@ export function getPersonalizedRewrite(pathname: string, variantIds: string[]):
// @public
export function getPersonalizedRewriteData(pathname: string): PersonalizedRewriteData;

// @public
export function getRenderingParamString(value: unknown): string | undefined;

// @public
const getRequiredParams: (qs: {
[key: string]: string | undefined;
Expand Down
1 change: 1 addition & 0 deletions packages/content/src/layout/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export {

export {
getFieldValue,
getRenderingParamString,
getChildPlaceholder,
isFieldValueEmpty,
isDynamicPlaceholder,
Expand Down
160 changes: 160 additions & 0 deletions packages/content/src/layout/themes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,166 @@ describe('themes', () => {
}))
);
});

// Layout Service may return Styles/CSSStyles/LibraryId params as objects
// rather than strings. Traversal must not crash on non-string values.
describe('non-string params (DetailedRenderingParams)', () => {
const detailedStylesObject = {
'Allowed Renderings': [],
IsVerifiedStyle: { value: false },
Value: { value: 'White-Background' },
Icon: { value: '' },
};

it('should not throw when params.Styles is an object', () => {
const run = () =>
getDesignLibraryStylesheetLinks(
setBasicLayoutData(({
componentName: 'styled',
params: { Styles: detailedStylesObject },
} as unknown) as ComponentRendering),
sitecoreEdgeContextId
);

expect(run).to.not.throw();
expect(run()).to.deep.equal([]);
});

it('should not throw when params.CSSStyles is an object', () => {
const run = () =>
getDesignLibraryStylesheetLinks(
setBasicLayoutData(({
componentName: 'styled',
params: { CSSStyles: detailedStylesObject },
} as unknown) as ComponentRendering),
sitecoreEdgeContextId
);

expect(run).to.not.throw();
expect(run()).to.deep.equal([]);
});

it('should not throw when params.LibraryId is an object', () => {
const run = () =>
getDesignLibraryStylesheetLinks(
setBasicLayoutData(({
componentName: 'styled',
params: { LibraryId: { Value: { value: 'bar' } } },
} as unknown) as ComponentRendering),
sitecoreEdgeContextId
);

expect(run).to.not.throw();
expect(run()).to.deep.equal([
{ href: getStylesheetUrl('bar', sitecoreEdgeContextId), rel: 'stylesheet' },
]);
});

it('should resolve library id from object Styles Value.value', () => {
expect(
getDesignLibraryStylesheetLinks(
setBasicLayoutData(({
componentName: 'styled',
params: {
Styles: {
Value: { value: '-library--foo White-Background' },
},
},
} as unknown) as ComponentRendering),
sitecoreEdgeContextId
)
).to.deep.equal([
{ href: getStylesheetUrl('foo', sitecoreEdgeContextId), rel: 'stylesheet' },
]);
});

it('should resolve library id from JSON string Styles param', () => {
expect(
getDesignLibraryStylesheetLinks(
setBasicLayoutData(({
componentName: 'styled',
params: {
Styles: '{"Value":{"value":"-library--foo"}}',
},
} as unknown) as ComponentRendering),
sitecoreEdgeContextId
)
).to.deep.equal([
{ href: getStylesheetUrl('foo', sitecoreEdgeContextId), rel: 'stylesheet' },
]);
});

it('should fall back to fields when params are objects', () => {
expect(
getDesignLibraryStylesheetLinks(
setBasicLayoutData(({
componentName: 'styled',
params: { Styles: detailedStylesObject },
fields: {
LibraryId: {
value: 'bar',
},
},
} as unknown) as ComponentRendering),
sitecoreEdgeContextId
)
).to.deep.equal([
{ href: getStylesheetUrl('bar', sitecoreEdgeContextId), rel: 'stylesheet' },
]);
});

it('should not throw when traversing nested placeholders with object params', () => {
const layoutData = {
sitecore: {
context: {},
route: {
name: 'home',
placeholders: {
main: [
{
componentName: 'header',
params: { Styles: detailedStylesObject },
placeholders: {
content: [
{
componentName: 'body',
params: {
CSSStyles: detailedStylesObject,
LibraryId: { IsVerifiedStyle: { value: false } },
},
},
],
},
},
],
},
},
},
};

const run = () => getDesignLibraryStylesheetLinks(layoutData, sitecoreEdgeContextId);

expect(run).to.not.throw();
expect(run()).to.deep.equal([]);
});

it('should still resolve library id from string CSSStyles when Styles param is an object', () => {
expect(
getDesignLibraryStylesheetLinks(
setBasicLayoutData(({
componentName: 'styled',
params: {
CSSStyles: '-library--foo',
Styles: detailedStylesObject,
},
} as unknown) as ComponentRendering),
sitecoreEdgeContextId
)
).to.deep.equal([
{ href: getStylesheetUrl('foo', sitecoreEdgeContextId), rel: 'stylesheet' },
]);
});
});
});

describe('getStylesheetUrl', () => {
Expand Down
30 changes: 23 additions & 7 deletions packages/content/src/layout/themes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { constants } from '@sitecore-content-sdk/core';
import { normalizeUrl } from '@sitecore-content-sdk/core/tools';
import { ComponentRendering, LayoutServiceData, RouteData, getFieldValue } from '.';
import {
ComponentRendering,
LayoutServiceData,
RouteData,
getFieldValue,
getRenderingParamString,
} from '.';
import { HTMLLink } from '../models';

/**
Expand Down Expand Up @@ -60,19 +66,29 @@ const traversePlaceholder = (components: ComponentRendering[], ids: Set<string>)
const traverseComponent = (component: RouteData | ComponentRendering, ids: Set<string>) => {
let libraryId: string | undefined = undefined;
if ('params' in component && component.params) {
const cssStylesParam = getRenderingParamString(component.params.CSSStyles);
const stylesParam = getRenderingParamString(component.params.Styles);
const libraryIdParam = getRenderingParamString(component.params.LibraryId);
// LibraryID in css class name takes precedence over LibraryId attribute
libraryId =
component.params.CSSStyles?.match(STYLES_LIBRARY_ID_REGEX)?.[1] ||
component.params.Styles?.match(STYLES_LIBRARY_ID_REGEX)?.[1] ||
component.params.LibraryId ||
cssStylesParam?.match(STYLES_LIBRARY_ID_REGEX)?.[1] ||
stylesParam?.match(STYLES_LIBRARY_ID_REGEX)?.[1] ||
libraryIdParam ||
undefined;
}
// if params are empty we try to fall back to data source
if (!libraryId && 'fields' in component && component.fields) {
const cssStylesField = getRenderingParamString(
getFieldValue(component.fields, 'CSSStyles', '')
);
const stylesField = getRenderingParamString(getFieldValue(component.fields, 'Styles', ''));
const libraryIdField = getRenderingParamString(
getFieldValue(component.fields, 'LibraryId', '')
);
libraryId =
getFieldValue(component.fields, 'CSSStyles', '').match(STYLES_LIBRARY_ID_REGEX)?.[1] ||
getFieldValue(component.fields, 'Styles', '').match(STYLES_LIBRARY_ID_REGEX)?.[1] ||
getFieldValue(component.fields, 'LibraryId', '') ||
cssStylesField?.match(STYLES_LIBRARY_ID_REGEX)?.[1] ||
stylesField?.match(STYLES_LIBRARY_ID_REGEX)?.[1] ||
libraryIdField ||
undefined;
}

Expand Down
37 changes: 37 additions & 0 deletions packages/content/src/layout/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { expect } from 'chai';
import { ComponentRendering } from '../../layout';
import {
getFieldValue,
getRenderingParamString,
getChildPlaceholder,
isFieldValueEmpty,
isDynamicPlaceholder,
Expand All @@ -11,6 +12,42 @@ import {
} from './utils';

describe('core layout utils', () => {
describe('getRenderingParamString', () => {
it('should return plain string params unchanged', () => {
expect(getRenderingParamString('col-lg-6')).to.equal('col-lg-6');
expect(getRenderingParamString('-library--foo')).to.equal('-library--foo');
});

it('should extract Value.value from DetailedRenderingParams objects', () => {
expect(
getRenderingParamString({
'Allowed Renderings': [],
IsVerifiedStyle: { value: false },
Value: { value: 'White-Background' },
Icon: { value: '' },
})
).to.equal('White-Background');
});

it('should extract value from simple object wrappers', () => {
expect(getRenderingParamString({ value: 'bar' })).to.equal('bar');
});

it('should extract Value.value from JSON string params', () => {
expect(
getRenderingParamString('{"Value":{"value":"White-Background"},"IsVerifiedStyle":{"value":false}}')
).to.equal('White-Background');
});

it('should return undefined for unextractable values', () => {
expect(getRenderingParamString(undefined)).to.be.undefined;
expect(getRenderingParamString(null)).to.be.undefined;
expect(getRenderingParamString({})).to.be.undefined;
expect(getRenderingParamString({ Value: { value: 42 } })).to.be.undefined;
expect(getRenderingParamString('{"foo":"bar"}')).to.be.undefined;
});
});

describe('getFieldValue', () => {
const fields = {
crop: {
Expand Down
61 changes: 61 additions & 0 deletions packages/content/src/layout/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,67 @@
/* eslint-disable no-redeclare */
import { ComponentRendering, ComponentFields, Field, GenericFieldValue } from './models';

const extractDetailedRenderingParamValue = (value: Record<string, unknown>): string | undefined => {
const detailedValue = value.Value;
if (
detailedValue &&
typeof detailedValue === 'object' &&
detailedValue !== null &&
'value' in detailedValue
) {
const nestedValue = (detailedValue as { value: unknown }).value;
if (typeof nestedValue === 'string') {
return nestedValue;
}
}

if (typeof value.value === 'string') {
return value.value;
}

return undefined;
};

/**
* Normalizes a rendering param value to a string.
* Layout Service may return DetailedRenderingParams as objects or JSON strings
* instead of plain strings (e.g. Styles, CSSStyles, GridParameters).
* @param {unknown} value rendering param value
* @returns {string | undefined} normalized string value, or undefined when not extractable
* @public
*/
export function getRenderingParamString(value: unknown): string | undefined {
if (value === null || value === undefined) {
return undefined;
}

if (typeof value === 'string') {
if (value.startsWith('{')) {
try {
const parsed: unknown = JSON.parse(value);
if (parsed && typeof parsed === 'object') {
const extracted = extractDetailedRenderingParamValue(parsed as Record<string, unknown>);
if (extracted !== undefined) {
return extracted;
}

return undefined;
}
} catch {
// Not JSON — treat as a plain string param value.
}
}

return value;
}

if (typeof value === 'object') {
return extractDetailedRenderingParamValue(value as Record<string, unknown>);
}

return undefined;
}

/**
* Safely extracts a field value from a rendering or fields object.
* Null will be returned if the field is not defined.
Expand Down
Loading
Loading