@salesforce/storefront-next-runtime 0.1.1 → 0.2.0-alpha.1
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/DesignComponent.js +150 -0
- package/dist/DesignComponent.js.map +1 -0
- package/dist/DesignFrame.js +196 -0
- package/dist/DesignFrame.js.map +1 -0
- package/dist/DesignRegion.js +83 -0
- package/dist/DesignRegion.js.map +1 -0
- package/dist/apply-url-config.js +130 -0
- package/dist/apply-url-config.js.map +1 -0
- package/dist/component.types.d.ts +87 -0
- package/dist/component.types.d.ts.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +0 -0
- package/dist/design-data.d.ts +983 -0
- package/dist/design-data.d.ts.map +1 -0
- package/dist/design-data.js +908 -0
- package/dist/design-data.js.map +1 -0
- package/dist/design-messaging.d.ts +2 -2
- package/dist/design-react-core.d.ts +48 -3
- package/dist/design-react-core.d.ts.map +1 -1
- package/dist/design-react-core.js +81 -2
- package/dist/design-react-core.js.map +1 -1
- package/dist/design-react.d.ts +20 -95
- package/dist/design-react.d.ts.map +1 -1
- package/dist/design-react.js +3 -485
- package/dist/design-styles.css +2 -1
- package/dist/design.d.ts +110 -2
- package/dist/design.d.ts.map +1 -0
- package/dist/events.d.ts +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/index.d.ts +1110 -154
- package/dist/index.d.ts.map +1 -1
- package/dist/multi-site.d.ts +154 -0
- package/dist/multi-site.d.ts.map +1 -0
- package/dist/multi-site.js +393 -0
- package/dist/multi-site.js.map +1 -0
- package/dist/routing-app-wrapper.d.ts +18 -0
- package/dist/routing-app-wrapper.d.ts.map +1 -0
- package/dist/routing-app-wrapper.js +21 -0
- package/dist/routing-app-wrapper.js.map +1 -0
- package/dist/routing.d.ts +42 -0
- package/dist/routing.d.ts.map +1 -0
- package/dist/routing.js +175 -0
- package/dist/routing.js.map +1 -0
- package/dist/scapi.d.ts +69 -5
- package/dist/scapi.d.ts.map +1 -1
- package/dist/scapi.js +1 -1
- package/dist/scapi.js.map +1 -1
- package/dist/types.d.ts +40 -13289
- package/dist/types.d.ts.map +1 -1
- package/dist/types2.d.ts +13293 -0
- package/dist/types2.d.ts.map +1 -0
- package/dist/types3.d.ts +110 -0
- package/dist/types3.d.ts.map +1 -0
- package/dist/workspace.d.ts +46 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +52 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +45 -2
- package/dist/design-react.js.map +0 -1
- package/dist/index2.d.ts +0 -1171
- package/dist/index2.d.ts.map +0 -1
- /package/{LICENSE.txt → LICENSE} +0 -0
|
@@ -0,0 +1,908 @@
|
|
|
1
|
+
//#region src/design/data/errors/visitor-context-error.ts
|
|
2
|
+
var VisitorContextError = class VisitorContextError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "VisitorContextError";
|
|
6
|
+
}
|
|
7
|
+
static assert(parentType, childType) {
|
|
8
|
+
if (parentType === "component" && childType !== "region" || parentType === "page" && childType !== "region" || parentType === "region" && childType !== "component") throw new VisitorContextError(`Invalid child context type ${childType} for parent context type ${parentType}`);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/design/data/page/transform.ts
|
|
14
|
+
/**
|
|
15
|
+
* Context object passed to {@link PageVisitor} handler methods during page tree
|
|
16
|
+
* traversal. Provides access to the current node via {@link node}, the tree
|
|
17
|
+
* position via {@link page}, {@link parentRegion}, and {@link parentComponent},
|
|
18
|
+
* and traversal methods ({@link visitRegions}, {@link visitComponents}) for
|
|
19
|
+
* continuing into child nodes.
|
|
20
|
+
*
|
|
21
|
+
* When a visitor handler is defined, the handler is responsible for traversing
|
|
22
|
+
* into children by calling the appropriate context method. If the handler does
|
|
23
|
+
* not call these methods, children will not be visited.
|
|
24
|
+
*/
|
|
25
|
+
var VisitorContext = class VisitorContext {
|
|
26
|
+
constructor(context) {
|
|
27
|
+
this.context = context;
|
|
28
|
+
}
|
|
29
|
+
get type() {
|
|
30
|
+
return this.context.type;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The current node being visited.
|
|
34
|
+
*/
|
|
35
|
+
get node() {
|
|
36
|
+
return this.context.node;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The root page being traversed.
|
|
40
|
+
*/
|
|
41
|
+
get page() {
|
|
42
|
+
return this.context.page;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The parent region of the current node, if traversing within a region.
|
|
46
|
+
*/
|
|
47
|
+
get parentRegion() {
|
|
48
|
+
return this.context.parentRegion;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The parent component of the current node, if traversing within a component's nested regions.
|
|
52
|
+
*/
|
|
53
|
+
get parentComponent() {
|
|
54
|
+
return this.context.parentComponent;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Traverses an array of regions, invoking the visitor's `visitRegion` handler
|
|
58
|
+
* on each one. Regions for which the handler returns `null` are excluded from
|
|
59
|
+
* the result. Call this from within a `visitPage` or `visitComponent` handler
|
|
60
|
+
* to continue traversal into child regions.
|
|
61
|
+
*
|
|
62
|
+
* @param regions - The regions to traverse.
|
|
63
|
+
* @returns The filtered array of transformed regions.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* transformPage(page, {
|
|
68
|
+
* visitPage(context) {
|
|
69
|
+
* // Traverse into regions explicitly
|
|
70
|
+
* const regions = context.visitRegions(context.node.regions);
|
|
71
|
+
* return { ...context.node, regions };
|
|
72
|
+
* },
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
visitRegions(regions = []) {
|
|
77
|
+
const newRegions = [];
|
|
78
|
+
for (const region of regions) {
|
|
79
|
+
const newRegion = this.visitRegion(region);
|
|
80
|
+
if (newRegion) newRegions.push(newRegion);
|
|
81
|
+
}
|
|
82
|
+
return newRegions;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Traverses a single region. If the visitor has a `visitRegion` handler, the
|
|
86
|
+
* handler is called with a new {@link VisitorContext} for the region. Otherwise,
|
|
87
|
+
* the region's child components are traversed automatically.
|
|
88
|
+
*
|
|
89
|
+
* @param region - The region to visit.
|
|
90
|
+
* @returns The transformed region, or `null` to exclude it.
|
|
91
|
+
*/
|
|
92
|
+
visitRegion(region) {
|
|
93
|
+
const regionContext = this.toChildContext("region", region);
|
|
94
|
+
if (this.context.visitor.visitRegion) return this.context.visitor.visitRegion(regionContext);
|
|
95
|
+
else if (region.components) return {
|
|
96
|
+
...region,
|
|
97
|
+
components: regionContext.visitComponents(region.components)
|
|
98
|
+
};
|
|
99
|
+
return region;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Traverses an array of components, invoking the visitor's `visitComponent`
|
|
103
|
+
* handler on each one. Components for which the handler returns `null` are
|
|
104
|
+
* excluded from the result. Call this from within a `visitRegion` handler to
|
|
105
|
+
* continue traversal into child components.
|
|
106
|
+
*
|
|
107
|
+
* @param components - The components to traverse.
|
|
108
|
+
* @returns The filtered array of transformed components.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* transformPage(page, {
|
|
113
|
+
* visitRegion(context) {
|
|
114
|
+
* // Traverse into components explicitly
|
|
115
|
+
* const components = context.visitComponents(context.node.components);
|
|
116
|
+
* return { ...context.node, components };
|
|
117
|
+
* },
|
|
118
|
+
* });
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
visitComponents(components = []) {
|
|
122
|
+
const newComponents = [];
|
|
123
|
+
for (const component of components) {
|
|
124
|
+
const newComponent = this.visitComponent(component);
|
|
125
|
+
if (newComponent) newComponents.push(newComponent);
|
|
126
|
+
}
|
|
127
|
+
return newComponents;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Traverses a single component. If the visitor has a `visitComponent` handler,
|
|
131
|
+
* the handler is called with a new {@link VisitorContext} for the component.
|
|
132
|
+
* Otherwise, the component's nested regions are traversed automatically.
|
|
133
|
+
*
|
|
134
|
+
* @param component - The component to visit.
|
|
135
|
+
* @returns The transformed component, or `null` to exclude it.
|
|
136
|
+
*/
|
|
137
|
+
visitComponent(component) {
|
|
138
|
+
const componentContext = this.toChildContext("component", component);
|
|
139
|
+
if (this.context.visitor.visitComponent) return this.context.visitor.visitComponent(componentContext);
|
|
140
|
+
else if (component.regions) return {
|
|
141
|
+
...component,
|
|
142
|
+
regions: componentContext.visitRegions(component.regions)
|
|
143
|
+
};
|
|
144
|
+
return component;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Traverses a single page. If the visitor has a `visitPage` handler, the
|
|
148
|
+
* handler is called with a new {@link VisitorContext} for the page. Otherwise,
|
|
149
|
+
* the page's regions are traversed automatically.
|
|
150
|
+
*
|
|
151
|
+
* @param page - The page to visit.
|
|
152
|
+
* @returns The transformed page, or `null` to exclude it.
|
|
153
|
+
*/
|
|
154
|
+
visitPage(page) {
|
|
155
|
+
const pageContext = new VisitorContext({
|
|
156
|
+
type: "page",
|
|
157
|
+
visitor: this.context.visitor,
|
|
158
|
+
page,
|
|
159
|
+
parentComponent: void 0,
|
|
160
|
+
parentRegion: void 0,
|
|
161
|
+
node: page
|
|
162
|
+
});
|
|
163
|
+
if (this.context.visitor.visitPage) return this.context.visitor.visitPage(pageContext);
|
|
164
|
+
else if (page.regions) return {
|
|
165
|
+
...page,
|
|
166
|
+
regions: pageContext.visitRegions(page.regions)
|
|
167
|
+
};
|
|
168
|
+
return page;
|
|
169
|
+
}
|
|
170
|
+
toChildContext(type, node) {
|
|
171
|
+
VisitorContextError.assert(this.context.type, type);
|
|
172
|
+
if (type === "region") return new VisitorContext({
|
|
173
|
+
type: "region",
|
|
174
|
+
visitor: this.context.visitor,
|
|
175
|
+
page: this.page,
|
|
176
|
+
node,
|
|
177
|
+
parentComponent: this.node,
|
|
178
|
+
parentRegion: this.parentRegion
|
|
179
|
+
});
|
|
180
|
+
return new VisitorContext({
|
|
181
|
+
type: "component",
|
|
182
|
+
visitor: this.context.visitor,
|
|
183
|
+
page: this.page,
|
|
184
|
+
node,
|
|
185
|
+
parentComponent: this.parentComponent,
|
|
186
|
+
parentRegion: this.node
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
var RootVisitorContext = class extends VisitorContext {
|
|
191
|
+
constructor(visitor) {
|
|
192
|
+
super({
|
|
193
|
+
node: null,
|
|
194
|
+
type: "root",
|
|
195
|
+
visitor
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* Traverses a page tree using the visitor pattern, applying the visitor's
|
|
201
|
+
* callbacks to the page, its regions, and their nested components. This is
|
|
202
|
+
* the top-level entry point for page tree transformation.
|
|
203
|
+
*
|
|
204
|
+
* When a visitor handler is defined, it receives a {@link VisitorContext} and
|
|
205
|
+
* is responsible for traversing into children using the context's traversal
|
|
206
|
+
* methods (`visitRegions`, `visitComponents`). If the handler does not call
|
|
207
|
+
* these methods, children will not be visited. When no handler is defined for
|
|
208
|
+
* a node type, children are traversed automatically.
|
|
209
|
+
*
|
|
210
|
+
* Returning `null` from a `visitRegion` or `visitComponent` callback removes
|
|
211
|
+
* that element and its children from the resulting tree.
|
|
212
|
+
*
|
|
213
|
+
* @param page - The page to traverse.
|
|
214
|
+
* @param visitor - The visitor with callbacks to apply at each tree node.
|
|
215
|
+
* @returns A new page with visitor transformations applied, or `null`.
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* ```ts
|
|
219
|
+
* import { transformPage } from '@salesforce/storefront-next-runtime/design/data';
|
|
220
|
+
*
|
|
221
|
+
* const page = { id: 'homepage', typeId: 'storePage', regions: [
|
|
222
|
+
* { id: 'header', components: [
|
|
223
|
+
* { id: 'hero-banner', typeId: 'commerce_assets.heroBanner', regions: [] },
|
|
224
|
+
* { id: 'promo-tile', typeId: 'commerce_assets.promoTile', regions: [] },
|
|
225
|
+
* ]},
|
|
226
|
+
* ]};
|
|
227
|
+
*
|
|
228
|
+
* // When only visitComponent is defined, regions are traversed automatically.
|
|
229
|
+
* // The handler receives a VisitorContext — use context.node to access the component.
|
|
230
|
+
* transformPage(page, {
|
|
231
|
+
* visitComponent(context) {
|
|
232
|
+
* console.log(`Component: ${context.node.typeId} in region ${context.parentRegion?.id}`);
|
|
233
|
+
* return context.node;
|
|
234
|
+
* },
|
|
235
|
+
* });
|
|
236
|
+
*
|
|
237
|
+
* // When visitRegion is defined, the handler must traverse into children explicitly.
|
|
238
|
+
* // Without calling context.visitComponents(), components inside the region are skipped.
|
|
239
|
+
* transformPage(page, {
|
|
240
|
+
* visitRegion(context) {
|
|
241
|
+
* console.log(`Entering region: ${context.node.id}`);
|
|
242
|
+
* const components = context.visitComponents(context.node.components);
|
|
243
|
+
* return { ...context.node, components };
|
|
244
|
+
* },
|
|
245
|
+
* visitComponent(context) {
|
|
246
|
+
* console.log(` Component: ${context.node.typeId}`);
|
|
247
|
+
* return context.node;
|
|
248
|
+
* },
|
|
249
|
+
* });
|
|
250
|
+
* ```
|
|
251
|
+
*/
|
|
252
|
+
function transformPage(page, visitor) {
|
|
253
|
+
return new RootVisitorContext(visitor).visitPage(page);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Applies the visitor to a single component. If the visitor's `visitComponent`
|
|
257
|
+
* handler is defined, it receives a {@link VisitorContext} and is responsible
|
|
258
|
+
* for traversing into the component's nested regions using `context.visitRegions()`.
|
|
259
|
+
* If no `visitComponent` handler is defined, nested regions are traversed
|
|
260
|
+
* automatically. Returns `null` to exclude the component from the result.
|
|
261
|
+
*
|
|
262
|
+
* @param component - The component to transform.
|
|
263
|
+
* @param visitor - The visitor with callbacks.
|
|
264
|
+
* @returns The transformed component, or `null` to exclude it.
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* ```ts
|
|
268
|
+
* import { transformComponent } from '@salesforce/storefront-next-runtime/design/data';
|
|
269
|
+
*
|
|
270
|
+
* // Replace the image URL in a hero banner component and traverse its nested regions
|
|
271
|
+
* const heroBanner = {
|
|
272
|
+
* id: 'hero-1',
|
|
273
|
+
* typeId: 'commerce_assets.heroBanner',
|
|
274
|
+
* data: { imageUrl: '/images/summer-sale.jpg' },
|
|
275
|
+
* regions: [{ id: 'banner-content', components: [] }],
|
|
276
|
+
* };
|
|
277
|
+
*
|
|
278
|
+
* const result = transformComponent(heroBanner, {
|
|
279
|
+
* visitComponent(context) {
|
|
280
|
+
* // Traverse into nested regions using the context API
|
|
281
|
+
* const regions = context.visitRegions(context.node.regions);
|
|
282
|
+
*
|
|
283
|
+
* if (context.node.typeId === 'commerce_assets.heroBanner') {
|
|
284
|
+
* return { ...context.node, regions, data: { ...context.node.data, imageUrl: '/images/winter-sale.jpg' } };
|
|
285
|
+
* }
|
|
286
|
+
* return { ...context.node, regions };
|
|
287
|
+
* },
|
|
288
|
+
* });
|
|
289
|
+
* ```
|
|
290
|
+
*/
|
|
291
|
+
function transformComponent(component, visitor) {
|
|
292
|
+
return new RootVisitorContext(visitor).visitComponent(component);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Applies the visitor to a single region. If the visitor's `visitRegion`
|
|
296
|
+
* handler is defined, it receives a {@link VisitorContext} and is responsible
|
|
297
|
+
* for traversing into the region's child components using `context.visitComponents()`.
|
|
298
|
+
* If no `visitRegion` handler is defined, child components are traversed
|
|
299
|
+
* automatically. Returns `null` to exclude the region and all its children
|
|
300
|
+
* from the result.
|
|
301
|
+
*
|
|
302
|
+
* @param region - The region to transform.
|
|
303
|
+
* @param visitor - The visitor with callbacks.
|
|
304
|
+
* @returns The transformed region, or `null` to exclude it.
|
|
305
|
+
*
|
|
306
|
+
* @example
|
|
307
|
+
* ```ts
|
|
308
|
+
* import { transformRegion } from '@salesforce/storefront-next-runtime/design/data';
|
|
309
|
+
*
|
|
310
|
+
* // Filter empty regions and traverse into non-empty ones
|
|
311
|
+
* const emptyRegion = { id: 'sidebar', components: [] };
|
|
312
|
+
* const populatedRegion = { id: 'main', components: [
|
|
313
|
+
* { id: 'product-grid', typeId: 'commerce_assets.productGrid', regions: [] },
|
|
314
|
+
* ]};
|
|
315
|
+
*
|
|
316
|
+
* const visitor = {
|
|
317
|
+
* visitRegion(context) {
|
|
318
|
+
* if (!context.node.components?.length) {
|
|
319
|
+
* return null; // Remove empty regions
|
|
320
|
+
* }
|
|
321
|
+
* // Traverse into child components using the context API
|
|
322
|
+
* const components = context.visitComponents(context.node.components);
|
|
323
|
+
* return { ...context.node, components };
|
|
324
|
+
* },
|
|
325
|
+
* };
|
|
326
|
+
*
|
|
327
|
+
* transformRegion(emptyRegion, visitor); // => null (removed)
|
|
328
|
+
* transformRegion(populatedRegion, visitor); // => { id: 'main', components: [...] }
|
|
329
|
+
* ```
|
|
330
|
+
*/
|
|
331
|
+
function transformRegion(region, visitor) {
|
|
332
|
+
return new RootVisitorContext(visitor).visitRegion(region);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
//#endregion
|
|
336
|
+
//#region src/design/data/page/resolve-data-bindings.ts
|
|
337
|
+
/**
|
|
338
|
+
* Pattern matching bare expressions: `type.field`.
|
|
339
|
+
*/
|
|
340
|
+
const BARE_EXPRESSION_PATTERN = /^(\w+)\.(\w+)$/;
|
|
341
|
+
/**
|
|
342
|
+
* Parses a binding expression string into its provider type and field name.
|
|
343
|
+
* Supports the bare `type.field` format.
|
|
344
|
+
*
|
|
345
|
+
* @param expression - The expression string to parse.
|
|
346
|
+
* @returns The parsed type and field, or `null` if the expression is invalid.
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* ```ts
|
|
350
|
+
* parseExpression('content_asset.title'); // { type: 'content_asset', field: 'title' }
|
|
351
|
+
* parseExpression('invalid'); // null
|
|
352
|
+
* ```
|
|
353
|
+
*/
|
|
354
|
+
function parseExpression(expression) {
|
|
355
|
+
const match = expression.trim().match(BARE_EXPRESSION_PATTERN);
|
|
356
|
+
if (match) return {
|
|
357
|
+
type: match[1],
|
|
358
|
+
field: match[2]
|
|
359
|
+
};
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Resolves a single binding expression against the component's data contexts
|
|
364
|
+
* and the resolved data bindings from context resolution.
|
|
365
|
+
*
|
|
366
|
+
* Returns the resolved field value, or an empty string if the expression is
|
|
367
|
+
* invalid, the matching context or record is not found, or the field does not
|
|
368
|
+
* exist on the resolved record.
|
|
369
|
+
*
|
|
370
|
+
* @param expression - The expression string (e.g. `"content_asset.body"`).
|
|
371
|
+
* @param contexts - The component's data binding contexts.
|
|
372
|
+
* @param dataBindings - The resolved data bindings from {@link QualifierContext}.
|
|
373
|
+
* @returns The resolved value, or `''` if resolution fails.
|
|
374
|
+
*/
|
|
375
|
+
function resolveExpression(expression, contexts, dataBindings) {
|
|
376
|
+
const parsed = parseExpression(expression);
|
|
377
|
+
if (!parsed) return "";
|
|
378
|
+
const context = contexts.find((c) => c.type === parsed.type);
|
|
379
|
+
if (!context) return "";
|
|
380
|
+
const record = dataBindings[context.type]?.[context.id];
|
|
381
|
+
if (!record) return "";
|
|
382
|
+
return record[parsed.field] ?? "";
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Extracts the {@link ComponentDataBinding} metadata from a component's
|
|
386
|
+
* `custom` field. Returns `undefined` if the component has no data binding
|
|
387
|
+
* configuration.
|
|
388
|
+
*/
|
|
389
|
+
function getDataBinding(component) {
|
|
390
|
+
return component.custom?.dataBinding;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Resolves data binding expressions for a single component. Replaces attribute
|
|
394
|
+
* values in the component's `data` with the resolved values from context
|
|
395
|
+
* resolution. Attributes without a matching expression are preserved as-is.
|
|
396
|
+
* When an expression cannot be resolved, the attribute value is set to an
|
|
397
|
+
* empty string.
|
|
398
|
+
*
|
|
399
|
+
* Returns the component unchanged if it has no data binding metadata or if
|
|
400
|
+
* `dataBindings` is `undefined`.
|
|
401
|
+
*
|
|
402
|
+
* @param component - The component to resolve data bindings for.
|
|
403
|
+
* @param dataBindings - The resolved data bindings from {@link QualifierContext}, or `undefined` if no bindings were resolved.
|
|
404
|
+
* @returns The component with resolved attribute values, or the original component if no bindings apply.
|
|
405
|
+
*
|
|
406
|
+
* @example
|
|
407
|
+
* ```ts
|
|
408
|
+
* import { resolveComponentDataBindings } from '@salesforce/storefront-next-runtime/design/data';
|
|
409
|
+
*
|
|
410
|
+
* const component = {
|
|
411
|
+
* id: 'banner',
|
|
412
|
+
* typeId: 'commerce_assets.contentBanner',
|
|
413
|
+
* data: { heading: 'Fallback Title', body: 'Fallback Body' },
|
|
414
|
+
* custom: {
|
|
415
|
+
* dataBinding: {
|
|
416
|
+
* expressions: {
|
|
417
|
+
* heading: 'content_asset.title',
|
|
418
|
+
* body: 'content_asset.body',
|
|
419
|
+
* },
|
|
420
|
+
* contexts: [{ type: 'content_asset', id: 'winter-sale-uuid' }],
|
|
421
|
+
* },
|
|
422
|
+
* },
|
|
423
|
+
* regions: [],
|
|
424
|
+
* };
|
|
425
|
+
*
|
|
426
|
+
* const dataBindings = {
|
|
427
|
+
* content_asset: {
|
|
428
|
+
* 'winter-sale-uuid': {
|
|
429
|
+
* title: 'Winter Sale',
|
|
430
|
+
* body: '<div>Free Shipping on all orders!</div>',
|
|
431
|
+
* },
|
|
432
|
+
* },
|
|
433
|
+
* };
|
|
434
|
+
*
|
|
435
|
+
* const resolved = resolveComponentDataBindings(component, dataBindings);
|
|
436
|
+
* // resolved.data.heading === 'Winter Sale'
|
|
437
|
+
* // resolved.data.body === '<div>Free Shipping on all orders!</div>'
|
|
438
|
+
* ```
|
|
439
|
+
*/
|
|
440
|
+
function resolveComponentDataBindings(component, dataBindings) {
|
|
441
|
+
if (!dataBindings) return component;
|
|
442
|
+
const binding = getDataBinding(component);
|
|
443
|
+
if (!binding?.contexts?.length) return component;
|
|
444
|
+
const expressionEntries = Object.entries(binding.expressions ?? {});
|
|
445
|
+
if (expressionEntries.length === 0) return component;
|
|
446
|
+
const resolvedData = { ...component.data };
|
|
447
|
+
for (const [attrName, expression] of expressionEntries) resolvedData[attrName] = resolveExpression(expression, binding.contexts, dataBindings);
|
|
448
|
+
return {
|
|
449
|
+
...component,
|
|
450
|
+
data: resolvedData
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
//#endregion
|
|
455
|
+
//#region src/design/data/validate-rule.ts
|
|
456
|
+
/**
|
|
457
|
+
* Evaluates a visibility rule against a shopper's qualifier context.
|
|
458
|
+
*
|
|
459
|
+
* Campaign-based and non-campaign rules are **mutually exclusive** paths,
|
|
460
|
+
* matching the server's `VisibilityDefinition.isVisible()` logic:
|
|
461
|
+
*
|
|
462
|
+
* - **Campaign-based rule** (has `campaignQualifiers`): only the campaign
|
|
463
|
+
* qualifiers are checked. Schedule and customer-group fields are ignored
|
|
464
|
+
* because the campaign qualification already incorporates those checks
|
|
465
|
+
* server-side.
|
|
466
|
+
* - **Non-campaign rule**: schedule AND customer groups are checked. All
|
|
467
|
+
* specified conditions must pass.
|
|
468
|
+
*
|
|
469
|
+
* When no context is provided and the rule requires campaign or customer group
|
|
470
|
+
* checks, those checks will fail (returning `false`). Schedule checks do not
|
|
471
|
+
* require context and are evaluated against `Date.now()`.
|
|
472
|
+
*
|
|
473
|
+
* @param rule - The visibility rule to evaluate.
|
|
474
|
+
* @param context - The shopper's active qualifiers, or `null`/`undefined` if not yet resolved.
|
|
475
|
+
* @returns `true` if the rule's conditions pass, `false` otherwise.
|
|
476
|
+
*
|
|
477
|
+
* @example
|
|
478
|
+
* ```ts
|
|
479
|
+
* import { validateRule } from '@salesforce/storefront-next-runtime/design/data';
|
|
480
|
+
*
|
|
481
|
+
* // Campaign-based rule — only campaign qualifiers are evaluated
|
|
482
|
+
* const campaignRule = {
|
|
483
|
+
* campaignQualifiers: [{ campaignId: 'holiday-sale-2026', promotionId: 'free-shipping' }],
|
|
484
|
+
* };
|
|
485
|
+
*
|
|
486
|
+
* // Non-campaign rule — schedule AND customer groups are evaluated
|
|
487
|
+
* const segmentRule = {
|
|
488
|
+
* customerGroups: ['vip-customers'],
|
|
489
|
+
* schedule: {
|
|
490
|
+
* start: new Date('2026-12-01').toISOString(),
|
|
491
|
+
* end: new Date('2026-12-31').toISOString(),
|
|
492
|
+
* },
|
|
493
|
+
* };
|
|
494
|
+
* ```
|
|
495
|
+
*/
|
|
496
|
+
function validateRule(rule, context) {
|
|
497
|
+
if (rule.campaignQualifiers) {
|
|
498
|
+
for (const campaignQualifier of rule.campaignQualifiers) if (!context?.campaignQualifiers[campaignQualifier.campaignId]?.[campaignQualifier.promotionId]) return false;
|
|
499
|
+
} else {
|
|
500
|
+
if (!rule.isActiveForLocale) return false;
|
|
501
|
+
if (rule.schedule) {
|
|
502
|
+
const now = Date.now();
|
|
503
|
+
if (rule.schedule.start) {
|
|
504
|
+
const startTimeInMillis = new Date(rule.schedule.start).getTime();
|
|
505
|
+
if (Number.isNaN(startTimeInMillis) || startTimeInMillis >= now) return false;
|
|
506
|
+
}
|
|
507
|
+
if (rule.schedule.end) {
|
|
508
|
+
const endTimeInMillis = new Date(rule.schedule.end).getTime();
|
|
509
|
+
if (Number.isNaN(endTimeInMillis) || endTimeInMillis <= now) return false;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (rule.customerGroups) {
|
|
513
|
+
for (const customerGroup of rule.customerGroups) if (!context?.customerGroups[customerGroup]) return false;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
//#endregion
|
|
520
|
+
//#region src/design/data/page/process-page.ts
|
|
521
|
+
/**
|
|
522
|
+
* Filters a page's components based on their visibility rules and resolves
|
|
523
|
+
* data binding expressions in a single traversal. Traverses the page tree
|
|
524
|
+
* using the visitor pattern and:
|
|
525
|
+
*
|
|
526
|
+
* 1. Removes any component whose visibility rules do not pass against the
|
|
527
|
+
* shopper's qualifier context.
|
|
528
|
+
* 2. Resolves data binding expressions in each surviving component's `data`
|
|
529
|
+
* attributes using the resolved data bindings from context resolution.
|
|
530
|
+
*
|
|
531
|
+
* A component is visible if **any** of its visibility rules pass (OR logic).
|
|
532
|
+
* If a component has rules and none of them pass, it is removed. Components
|
|
533
|
+
* without rules are always included.
|
|
534
|
+
*
|
|
535
|
+
* @param page - The page to process.
|
|
536
|
+
* @param context - The processing context with qualifier data, visibility rules, and resolved data bindings.
|
|
537
|
+
* @returns A new page with invisible components filtered out and data binding expressions resolved.
|
|
538
|
+
*
|
|
539
|
+
* @example
|
|
540
|
+
* ```ts
|
|
541
|
+
* import { processPage } from '@salesforce/storefront-next-runtime/design/data';
|
|
542
|
+
*
|
|
543
|
+
* const page = {
|
|
544
|
+
* id: 'homepage',
|
|
545
|
+
* typeId: 'storePage',
|
|
546
|
+
* regions: [{
|
|
547
|
+
* id: 'main',
|
|
548
|
+
* components: [
|
|
549
|
+
* { id: 'public-banner', typeId: 'commerce_assets.heroBanner', regions: [] },
|
|
550
|
+
* { id: 'loyalty-offer', typeId: 'commerce_assets.promoTile', regions: [] },
|
|
551
|
+
* ],
|
|
552
|
+
* }],
|
|
553
|
+
* };
|
|
554
|
+
*
|
|
555
|
+
* // The "loyalty-offer" component requires the shopper to be in "loyalty-members"
|
|
556
|
+
* const componentInfo = {
|
|
557
|
+
* 'public-banner': { visibilityRules: [], hasVisibilityRules: false },
|
|
558
|
+
* 'loyalty-offer': {
|
|
559
|
+
* visibilityRules: [{ customerGroups: ['loyalty-members'] }],
|
|
560
|
+
* hasVisibilityRules: true,
|
|
561
|
+
* },
|
|
562
|
+
* };
|
|
563
|
+
*
|
|
564
|
+
* // Guest shopper — not in any customer group
|
|
565
|
+
* const filtered = processPage(page, {
|
|
566
|
+
* qualifiers: { customerGroups: {}, campaignQualifiers: {} },
|
|
567
|
+
* componentInfo,
|
|
568
|
+
* });
|
|
569
|
+
* // filtered.regions[0].components has only "public-banner"
|
|
570
|
+
* // "loyalty-offer" was removed because the shopper isn't a loyalty member
|
|
571
|
+
* ```
|
|
572
|
+
*/
|
|
573
|
+
function processPage(page, processorContext) {
|
|
574
|
+
return transformPage(page, { visitComponent(ctx) {
|
|
575
|
+
const componentInfo = processorContext.componentInfo[ctx.node.id];
|
|
576
|
+
const visibilityRules = componentInfo?.visibilityRules ?? [];
|
|
577
|
+
if (visibilityRules.length > 0) {
|
|
578
|
+
if (!visibilityRules.some((rule) => validateRule(rule, processorContext.qualifiers))) return null;
|
|
579
|
+
}
|
|
580
|
+
const resolved = resolveComponentDataBindings(ctx.node, processorContext.qualifiers?.dataBindings);
|
|
581
|
+
if (!componentInfo || componentInfo.hasAnyDescendantVisibilityRules || componentInfo.hasAnyDescendantDataBindings) return {
|
|
582
|
+
...resolved,
|
|
583
|
+
regions: ctx.visitRegions(ctx.node.regions)
|
|
584
|
+
};
|
|
585
|
+
return resolved;
|
|
586
|
+
} });
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
//#endregion
|
|
590
|
+
//#region src/design/data/errors/required.ts
|
|
591
|
+
/**
|
|
592
|
+
* Copyright 2026 Salesforce, Inc.
|
|
593
|
+
*
|
|
594
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
595
|
+
* you may not use this file except in compliance with the License.
|
|
596
|
+
* You may obtain a copy of the License at
|
|
597
|
+
*
|
|
598
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
599
|
+
*
|
|
600
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
601
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
602
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
603
|
+
* See the License for the specific language governing permissions and
|
|
604
|
+
* limitations under the License.
|
|
605
|
+
*/
|
|
606
|
+
var RequiredError = class RequiredError extends Error {
|
|
607
|
+
constructor(message) {
|
|
608
|
+
super(message);
|
|
609
|
+
this.name = "RequiredError";
|
|
610
|
+
}
|
|
611
|
+
static assert(value, message, isEmpty = (v) => v == null) {
|
|
612
|
+
if (isEmpty(value)) throw new RequiredError(message);
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
//#endregion
|
|
617
|
+
//#region src/design/data/manifest/content-assignment-resolvers.ts
|
|
618
|
+
/**
|
|
619
|
+
* Registry of content assignment resolvers keyed by {@link IdentifierType}.
|
|
620
|
+
* Each resolver knows how to convert its identifier type into a set of lookup
|
|
621
|
+
* keys for the site manifest.
|
|
622
|
+
*
|
|
623
|
+
* Built-in resolvers:
|
|
624
|
+
* - **`'product'`** — Maps a product ID to a single PDP lookup key.
|
|
625
|
+
* - **`'category'`** — Maps a category ID to an ordered list of keys that
|
|
626
|
+
* traverses the category hierarchy from child to root, enabling inherited
|
|
627
|
+
* page assignments.
|
|
628
|
+
*
|
|
629
|
+
* The `'page'` identifier type has no resolver — page IDs are used directly.
|
|
630
|
+
*
|
|
631
|
+
* @example
|
|
632
|
+
* ```ts
|
|
633
|
+
* import { ContentAssignmentResolvers } from '@salesforce/storefront-next-runtime/design/data';
|
|
634
|
+
*
|
|
635
|
+
* // Resolve a product identifier for PDP lookup
|
|
636
|
+
* const productResolver = ContentAssignmentResolvers.get('product');
|
|
637
|
+
* productResolver('nike-air-max-90');
|
|
638
|
+
* // => { objectType: 'product', aspectType: 'pdp', keys: ['nike-air-max-90'] }
|
|
639
|
+
*
|
|
640
|
+
* // Resolve a category identifier — traverses hierarchy to find inherited assignments
|
|
641
|
+
* const categoryResolver = ContentAssignmentResolvers.get('category');
|
|
642
|
+
* const siteManifest = {
|
|
643
|
+
* categories: {
|
|
644
|
+
* 'mens-running-shoes': { name: 'Running Shoes', parentCategory: 'mens-shoes' },
|
|
645
|
+
* 'mens-shoes': { name: "Men's Shoes", parentCategory: 'mens' },
|
|
646
|
+
* 'mens': { name: 'Men' },
|
|
647
|
+
* },
|
|
648
|
+
* contentObjectAssignments: {},
|
|
649
|
+
* };
|
|
650
|
+
* categoryResolver('mens-running-shoes', siteManifest);
|
|
651
|
+
* // => { objectType: 'category', aspectType: 'plp', keys: ['mens-running-shoes', 'mens-shoes', 'mens'] }
|
|
652
|
+
* ```
|
|
653
|
+
*/
|
|
654
|
+
const ContentAssignmentResolvers = new Map([["product", (key) => ({
|
|
655
|
+
objectType: "product",
|
|
656
|
+
keys: [key]
|
|
657
|
+
})], ["category", (key, manifest) => {
|
|
658
|
+
const keys = [];
|
|
659
|
+
const visited = /* @__PURE__ */ new Set();
|
|
660
|
+
let currentCategoryId = key;
|
|
661
|
+
while (currentCategoryId && !visited.has(currentCategoryId)) {
|
|
662
|
+
visited.add(currentCategoryId);
|
|
663
|
+
keys.push(currentCategoryId);
|
|
664
|
+
currentCategoryId = manifest?.categories[currentCategoryId]?.parentCategory;
|
|
665
|
+
}
|
|
666
|
+
return {
|
|
667
|
+
objectType: "category",
|
|
668
|
+
keys
|
|
669
|
+
};
|
|
670
|
+
}]]);
|
|
671
|
+
|
|
672
|
+
//#endregion
|
|
673
|
+
//#region src/design/data/manifest/resolve-dynamic-page-id.ts
|
|
674
|
+
/**
|
|
675
|
+
* Converts a product or category identifier into a page ID by looking up
|
|
676
|
+
* content assignments in the site manifest. For categories, the lookup
|
|
677
|
+
* traverses the category hierarchy from the given category up to the root,
|
|
678
|
+
* returning the first matching assignment.
|
|
679
|
+
*
|
|
680
|
+
* Returns `null` if no content assignment is found for the identifier or if
|
|
681
|
+
* the identifier type has no registered resolver.
|
|
682
|
+
*
|
|
683
|
+
* @param options - The resolution options.
|
|
684
|
+
* @param options.id - The identifier to resolve (product ID, category ID, or page ID).
|
|
685
|
+
* @param options.identifierType - The type of identifier: `'product'`, `'category'`, or `'page'`.
|
|
686
|
+
* @param options.siteManifest - The site manifest containing content assignments and category hierarchy.
|
|
687
|
+
* @returns The resolved page ID, or `null` if no assignment was found.
|
|
688
|
+
*
|
|
689
|
+
* @example
|
|
690
|
+
* ```ts
|
|
691
|
+
* import { resolveDynamicPageId } from '@salesforce/storefront-next-runtime/design/data';
|
|
692
|
+
*
|
|
693
|
+
* const siteManifest = {
|
|
694
|
+
* contentObjectAssignments: {
|
|
695
|
+
* plp: {
|
|
696
|
+
* category: {
|
|
697
|
+
* 'mens-shoes': {
|
|
698
|
+
* lookupMode: 'category-explicit',
|
|
699
|
+
* contentId: 'page-mens-shoes-plp',
|
|
700
|
+
* },
|
|
701
|
+
* },
|
|
702
|
+
* },
|
|
703
|
+
* },
|
|
704
|
+
* categories: {
|
|
705
|
+
* 'mens-running-shoes': { name: 'Running Shoes', parentCategory: 'mens-shoes' },
|
|
706
|
+
* 'mens-shoes': { name: "Men's Shoes" },
|
|
707
|
+
* },
|
|
708
|
+
* };
|
|
709
|
+
*
|
|
710
|
+
* // Direct match
|
|
711
|
+
* resolveDynamicPageId({ id: 'mens-shoes', identifierType: 'category', siteManifest });
|
|
712
|
+
* // => 'page-mens-shoes-plp'
|
|
713
|
+
*
|
|
714
|
+
* // Inherited from parent category
|
|
715
|
+
* resolveDynamicPageId({ id: 'mens-running-shoes', identifierType: 'category', siteManifest });
|
|
716
|
+
* // => 'page-mens-shoes-plp' (found via parent traversal)
|
|
717
|
+
*
|
|
718
|
+
* // No assignment found
|
|
719
|
+
* resolveDynamicPageId({ id: 'womens-shoes', identifierType: 'category', siteManifest });
|
|
720
|
+
* // => null
|
|
721
|
+
* ```
|
|
722
|
+
*/
|
|
723
|
+
function resolveDynamicPageId({ id, identifierType, siteManifest, aspectType }) {
|
|
724
|
+
const resolvedContentAssignmentLookup = ContentAssignmentResolvers.get(identifierType)?.(id, siteManifest);
|
|
725
|
+
if (resolvedContentAssignmentLookup) for (const key of resolvedContentAssignmentLookup.keys) {
|
|
726
|
+
const contentAssignment = siteManifest?.contentObjectAssignments?.[aspectType]?.[resolvedContentAssignmentLookup.objectType]?.[key];
|
|
727
|
+
if (contentAssignment) return contentAssignment.contentId;
|
|
728
|
+
}
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
//#endregion
|
|
733
|
+
//#region src/design/data/manifest/get-page.ts
|
|
734
|
+
/**
|
|
735
|
+
* Selects the appropriate page variation from a manifest by evaluating each
|
|
736
|
+
* variation's visibility rule in order. Returns the first variation whose rule
|
|
737
|
+
* passes, or falls back to the manifest's default variation.
|
|
738
|
+
*
|
|
739
|
+
* The qualifier context is resolved lazily — the `contextResolver` is only
|
|
740
|
+
* called when a variation's `ruleRequiresContext` flag is `true`, and only
|
|
741
|
+
* once (the result is cached for subsequent variations).
|
|
742
|
+
*
|
|
743
|
+
* @param manifest - The page manifest containing all variations.
|
|
744
|
+
* @param options - Resolution options.
|
|
745
|
+
* @param options.contextResolver - Optional async function that returns the shopper's qualifier context. Only called if a variation's rule needs it.
|
|
746
|
+
* @returns The selected variation entry and resolved context, or `null` if no variation (including default) exists.
|
|
747
|
+
*
|
|
748
|
+
* @example
|
|
749
|
+
* ```ts
|
|
750
|
+
* import { getPageFromManifest } from '@salesforce/storefront-next-runtime/design/data';
|
|
751
|
+
*
|
|
752
|
+
* const manifest = {
|
|
753
|
+
* pageId: 'homepage',
|
|
754
|
+
* locale: 'en-US',
|
|
755
|
+
* context: { campaignQualifiers: [], customerGroups: ['vip-customers'], dataBindings: [] },
|
|
756
|
+
* variationOrder: ['vip-homepage', 'holiday-homepage'],
|
|
757
|
+
* variations: {
|
|
758
|
+
* 'vip-homepage': {
|
|
759
|
+
* ruleRequiresContext: true,
|
|
760
|
+
* pageRequiresContext: false,
|
|
761
|
+
* visibilityRule: { customerGroups: ['vip-customers'] },
|
|
762
|
+
* page: { id: 'homepage', typeId: 'storePage', regions: [] },
|
|
763
|
+
* },
|
|
764
|
+
* 'holiday-homepage': {
|
|
765
|
+
* ruleRequiresContext: false,
|
|
766
|
+
* pageRequiresContext: false,
|
|
767
|
+
* visibilityRule: {
|
|
768
|
+
* schedule: {
|
|
769
|
+
* start: new Date('2026-12-01').getTime(),
|
|
770
|
+
* end: new Date('2026-12-31').getTime(),
|
|
771
|
+
* },
|
|
772
|
+
* },
|
|
773
|
+
* page: { id: 'homepage', typeId: 'storePage', regions: [] },
|
|
774
|
+
* },
|
|
775
|
+
* 'default-homepage': {
|
|
776
|
+
* ruleRequiresContext: false,
|
|
777
|
+
* pageRequiresContext: false,
|
|
778
|
+
* page: { id: 'homepage', typeId: 'storePage', regions: [] },
|
|
779
|
+
* },
|
|
780
|
+
* },
|
|
781
|
+
* defaultVariation: 'default-homepage',
|
|
782
|
+
* visibilityRules: {},
|
|
783
|
+
* };
|
|
784
|
+
*
|
|
785
|
+
* // VIP shopper — matches first variation
|
|
786
|
+
* const result = await getPageFromManifest(manifest, {
|
|
787
|
+
* contextResolver: async () => ({
|
|
788
|
+
* customerGroups: { 'vip-customers': true },
|
|
789
|
+
* campaignQualifiers: {},
|
|
790
|
+
* }),
|
|
791
|
+
* });
|
|
792
|
+
* // result.entry === manifest.variations['vip-homepage']
|
|
793
|
+
*
|
|
794
|
+
* // Non-VIP shopper outside holiday window — falls back to default
|
|
795
|
+
* const fallback = await getPageFromManifest(manifest, {
|
|
796
|
+
* contextResolver: async () => ({
|
|
797
|
+
* customerGroups: {},
|
|
798
|
+
* campaignQualifiers: {},
|
|
799
|
+
* }),
|
|
800
|
+
* });
|
|
801
|
+
* // fallback.entry === manifest.variations['default-homepage']
|
|
802
|
+
* ```
|
|
803
|
+
*/
|
|
804
|
+
async function getPageFromManifest(manifest, { contextResolver }) {
|
|
805
|
+
let context = null;
|
|
806
|
+
let resolvedVariation = null;
|
|
807
|
+
for (const variationId of manifest.variationOrder) {
|
|
808
|
+
const variation = manifest.variations[variationId];
|
|
809
|
+
if (variation?.ruleRequiresContext && !context) context = await contextResolver?.() ?? null;
|
|
810
|
+
if (!variation?.visibilityRule || validateRule(variation.visibilityRule, context)) {
|
|
811
|
+
resolvedVariation = variation;
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
if (!resolvedVariation) resolvedVariation = manifest.variations[manifest.defaultVariation];
|
|
816
|
+
if (!resolvedVariation) return null;
|
|
817
|
+
return {
|
|
818
|
+
entry: resolvedVariation,
|
|
819
|
+
context
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
//#endregion
|
|
824
|
+
//#region src/design/data/page/resolve-page.ts
|
|
825
|
+
/**
|
|
826
|
+
* Main entry point for the page resolution pipeline. Orchestrates the full flow:
|
|
827
|
+
*
|
|
828
|
+
* 1. **Resolve dynamic page ID** — For product/category identifiers, looks up
|
|
829
|
+
* the assigned page ID via content assignments in the site manifest.
|
|
830
|
+
* 2. **Fetch page manifest** — Loads all variations for the resolved page.
|
|
831
|
+
* 3. **Select variation** — Evaluates visibility rules to pick the right variation.
|
|
832
|
+
* 4. **Load qualifier context** — Lazily fetches the shopper's context only if needed.
|
|
833
|
+
* 5. **Process page** — Filters out components that fail visibility rules.
|
|
834
|
+
*
|
|
835
|
+
* Returns `null` if the page ID cannot be resolved, the manifest doesn't exist,
|
|
836
|
+
* or no variation is available.
|
|
837
|
+
*
|
|
838
|
+
* @param options - The resolution options.
|
|
839
|
+
* @param options.id - The identifier to resolve (product ID, category ID, or page ID).
|
|
840
|
+
* @param options.identifierType - The type of identifier: `'product'`, `'category'`, or `'page'`.
|
|
841
|
+
* @param options.locale - The locale to resolve the page for (e.g. `"en-US"`).
|
|
842
|
+
* @param options.manifestStorage - Storage implementation for fetching manifests.
|
|
843
|
+
* @param options.contextResolver - Optional async function that returns the shopper's qualifier context. Only called if a visibility rule needs it.
|
|
844
|
+
* @param options.aspectType - The aspect type to resolve the page for when the identifier type is `'product'` or `'category'`.
|
|
845
|
+
* @returns The fully resolved and filtered page, or `null`.
|
|
846
|
+
*
|
|
847
|
+
* @example
|
|
848
|
+
* ```ts
|
|
849
|
+
* import { resolvePage } from '@salesforce/storefront-next-runtime/design/data';
|
|
850
|
+
*
|
|
851
|
+
* // Resolve the PDP page for a specific product with an active holiday campaign
|
|
852
|
+
* const page = await resolvePage({
|
|
853
|
+
* id: 'nike-air-max-90',
|
|
854
|
+
* identifierType: 'product',
|
|
855
|
+
* aspectType: 'pdp',
|
|
856
|
+
* locale: 'en-US',
|
|
857
|
+
* manifestStorage: {
|
|
858
|
+
* async getPageManifest(id, locale) {
|
|
859
|
+
* // Fetch from CDN, filesystem, or database
|
|
860
|
+
* return fetchManifest(`/manifests/${locale}/${id}.json`);
|
|
861
|
+
* },
|
|
862
|
+
* async getSiteManifest(locale) {
|
|
863
|
+
* return fetchManifest(`/manifests/${locale}/site.json`);
|
|
864
|
+
* },
|
|
865
|
+
* },
|
|
866
|
+
* contextResolver: async () => ({
|
|
867
|
+
* customerGroups: { 'vip-customers': true },
|
|
868
|
+
* campaignQualifiers: {
|
|
869
|
+
* 'holiday-sale-2026': { 'free-shipping': true },
|
|
870
|
+
* },
|
|
871
|
+
* }),
|
|
872
|
+
* });
|
|
873
|
+
*
|
|
874
|
+
* if (page) {
|
|
875
|
+
* // page.regions contains only components visible to this VIP shopper
|
|
876
|
+
* // during the holiday sale campaign
|
|
877
|
+
* renderPage(page);
|
|
878
|
+
* }
|
|
879
|
+
* ```
|
|
880
|
+
*/
|
|
881
|
+
async function resolvePage({ id, identifierType, aspectType, locale, manifestStorage, contextResolver }) {
|
|
882
|
+
let resolvedId = null;
|
|
883
|
+
if (ContentAssignmentResolvers.has(identifierType)) {
|
|
884
|
+
const siteManifest = await manifestStorage.getSiteManifest(locale);
|
|
885
|
+
RequiredError.assert(aspectType, `Aspect type is required for identifier type ${identifierType}`, (v) => !v);
|
|
886
|
+
resolvedId = resolveDynamicPageId({
|
|
887
|
+
id,
|
|
888
|
+
identifierType,
|
|
889
|
+
aspectType,
|
|
890
|
+
siteManifest
|
|
891
|
+
});
|
|
892
|
+
} else resolvedId = id;
|
|
893
|
+
if (!resolvedId) return null;
|
|
894
|
+
const pageManifest = await manifestStorage.getPageManifest(resolvedId, locale);
|
|
895
|
+
if (!pageManifest) return null;
|
|
896
|
+
const pageResults = await getPageFromManifest(pageManifest, { contextResolver });
|
|
897
|
+
if (!pageResults) return null;
|
|
898
|
+
let context = null;
|
|
899
|
+
if (pageResults.entry.pageRequiresContext) context = pageResults.context ?? await contextResolver?.() ?? null;
|
|
900
|
+
return processPage(pageResults.entry.page, {
|
|
901
|
+
qualifiers: context,
|
|
902
|
+
componentInfo: pageManifest.componentInfo
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
//#endregion
|
|
907
|
+
export { ContentAssignmentResolvers, RequiredError, getPageFromManifest, parseExpression, processPage, resolveComponentDataBindings, resolveDynamicPageId, resolveExpression, resolvePage, transformComponent, transformPage, transformRegion, validateRule };
|
|
908
|
+
//# sourceMappingURL=design-data.js.map
|