@salesforce/storefront-next-runtime 0.1.1-alpha.0 → 0.2.0-alpha.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.
Files changed (61) hide show
  1. package/dist/DesignComponent.js +150 -0
  2. package/dist/DesignComponent.js.map +1 -0
  3. package/dist/DesignFrame.js +196 -0
  4. package/dist/DesignFrame.js.map +1 -0
  5. package/dist/DesignRegion.js +83 -0
  6. package/dist/DesignRegion.js.map +1 -0
  7. package/dist/apply-url-config.js +130 -0
  8. package/dist/apply-url-config.js.map +1 -0
  9. package/dist/component.types.d.ts +87 -0
  10. package/dist/component.types.d.ts.map +1 -0
  11. package/dist/config.d.ts +2 -0
  12. package/dist/config.js +0 -0
  13. package/dist/design-data.d.ts +983 -0
  14. package/dist/design-data.d.ts.map +1 -0
  15. package/dist/design-data.js +908 -0
  16. package/dist/design-data.js.map +1 -0
  17. package/dist/design-messaging.d.ts +2 -2
  18. package/dist/design-react-core.d.ts +50 -5
  19. package/dist/design-react-core.d.ts.map +1 -1
  20. package/dist/design-react-core.js +81 -2
  21. package/dist/design-react-core.js.map +1 -1
  22. package/dist/design-react.d.ts +20 -95
  23. package/dist/design-react.d.ts.map +1 -1
  24. package/dist/design-react.js +3 -485
  25. package/dist/design-styles.css +2 -1
  26. package/dist/design.d.ts +110 -2
  27. package/dist/design.d.ts.map +1 -0
  28. package/dist/events.d.ts +1 -1
  29. package/dist/index.d.ts +1110 -154
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/multi-site.d.ts +154 -0
  32. package/dist/multi-site.d.ts.map +1 -0
  33. package/dist/multi-site.js +393 -0
  34. package/dist/multi-site.js.map +1 -0
  35. package/dist/routing-app-wrapper.d.ts +18 -0
  36. package/dist/routing-app-wrapper.d.ts.map +1 -0
  37. package/dist/routing-app-wrapper.js +21 -0
  38. package/dist/routing-app-wrapper.js.map +1 -0
  39. package/dist/routing.d.ts +42 -0
  40. package/dist/routing.d.ts.map +1 -0
  41. package/dist/routing.js +171 -0
  42. package/dist/routing.js.map +1 -0
  43. package/dist/scapi.d.ts +69 -5
  44. package/dist/scapi.d.ts.map +1 -1
  45. package/dist/scapi.js +1 -1
  46. package/dist/scapi.js.map +1 -1
  47. package/dist/types.d.ts +40 -13289
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/types2.d.ts +13293 -0
  50. package/dist/types2.d.ts.map +1 -0
  51. package/dist/types3.d.ts +110 -0
  52. package/dist/types3.d.ts.map +1 -0
  53. package/dist/workspace.d.ts +46 -0
  54. package/dist/workspace.d.ts.map +1 -0
  55. package/dist/workspace.js +52 -0
  56. package/dist/workspace.js.map +1 -0
  57. package/package.json +44 -2
  58. package/dist/design-react.js.map +0 -1
  59. package/dist/index2.d.ts +0 -1171
  60. package/dist/index2.d.ts.map +0 -1
  61. /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