@salesforce/storefront-next-runtime 0.4.1 → 1.0.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.
- package/README.md +9 -3
- package/dist/config.d.ts +33 -221
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +34 -116
- package/dist/config.js.map +1 -1
- package/dist/data-store.d.ts +185 -15
- package/dist/data-store.d.ts.map +1 -1
- package/dist/data-store.js +412 -10
- package/dist/data-store.js.map +1 -1
- package/dist/design-data.d.ts +266 -62
- package/dist/design-data.d.ts.map +1 -1
- package/dist/design-data.js +399 -14
- package/dist/design-data.js.map +1 -1
- package/dist/design-mode.d.ts +3 -2
- package/dist/design-mode.d.ts.map +1 -1
- package/dist/events.d.ts +32 -6
- package/dist/events.d.ts.map +1 -1
- package/dist/i18n-client.d.ts.map +1 -1
- package/dist/i18n-client.js.map +1 -1
- package/dist/i18n.d.ts +1 -2
- package/dist/i18n.d.ts.map +1 -1
- package/dist/modeDetection.js +0 -18
- package/dist/modeDetection.js.map +1 -1
- package/dist/scapi.d.ts +2185 -466
- package/dist/scapi.d.ts.map +1 -1
- package/dist/scapi.js +1 -1
- package/dist/scapi.js.map +1 -1
- package/dist/schema.d.ts +17 -15
- package/dist/schema.d.ts.map +1 -1
- package/dist/site-context.d.ts +43 -27
- package/dist/site-context.d.ts.map +1 -1
- package/dist/site-context.js +2 -2
- package/dist/site-context2.js +41 -31
- package/dist/site-context2.js.map +1 -1
- package/dist/types.d.ts +19 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types2.d.ts +89 -63
- package/dist/types2.d.ts.map +1 -1
- package/package.json +2 -20
- package/dist/custom-global-preferences.d.ts +0 -20
- package/dist/custom-global-preferences.d.ts.map +0 -1
- package/dist/custom-global-preferences.js +0 -31
- package/dist/custom-global-preferences.js.map +0 -1
- package/dist/custom-site-preferences.d.ts +0 -20
- package/dist/custom-site-preferences.d.ts.map +0 -1
- package/dist/custom-site-preferences.js +0 -31
- package/dist/custom-site-preferences.js.map +0 -1
- package/dist/data-store-custom-global-preferences.d.ts +0 -2
- package/dist/data-store-custom-global-preferences.js +0 -6
- package/dist/data-store-custom-site-preferences.d.ts +0 -2
- package/dist/data-store-custom-site-preferences.js +0 -6
- package/dist/data-store-gcp-preferences.d.ts +0 -2
- package/dist/data-store-gcp-preferences.js +0 -6
- package/dist/gcp-preferences.d.ts +0 -52
- package/dist/gcp-preferences.d.ts.map +0 -1
- package/dist/gcp-preferences.js +0 -64
- package/dist/gcp-preferences.js.map +0 -1
- package/dist/utils.js +0 -90
- package/dist/utils.js.map +0 -1
package/dist/design-data.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"design-data.js","names":["context: {\n /** The current node being visited. */\n node: TNode;\n /** The node type */\n type: VisitorContextType;\n /** The visitor being used to transform the page tree. */\n visitor: PageVisitor;\n /** The root page being traversed. */\n page?: ShopperExperience.schemas['Page'];\n /** The parent visitor context, providing access to the node that contains the current one in the page tree. */\n parent?: VisitorContext<\n | ShopperExperience.schemas['Page']\n | ShopperExperience.schemas['Region']\n | ShopperExperience.schemas['Component']\n >;\n /** The parent region of the current node, if traversing within a region. */\n parentRegion?: ShopperExperience.schemas['Region'];\n /** The parent component of the current node, if traversing within a component's nested regions. */\n parentComponent?: ShopperExperience.schemas['Component'];\n }","record: ResolvedDataBinding | undefined","resolvedData: Record<string, unknown>","regionInfo: RegionInfo | undefined","result: ShopperExperience.schemas['Component'][]","node: ShopperExperience.schemas['Component']","currentCategoryId: string | undefined","context: QualifierContext | null","resolvedVariation: VariationEntry | null","resolvedId: string | null","context: QualifierContext | null"],"sources":["../src/design/data/errors/visitor-context-error.ts","../src/design/data/page/transform.ts","../src/design/data/page/resolve-data-bindings.ts","../src/design/data/validate-rule.ts","../src/design/data/page/process-page.ts","../src/design/data/errors/required.ts","../src/design/data/manifest/content-assignment-resolvers.ts","../src/design/data/manifest/resolve-dynamic-page-id.ts","../src/design/data/manifest/get-page.ts","../src/design/data/page/resolve-page.ts"],"sourcesContent":["/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { VisitorContextType } from '../types';\n\nexport class VisitorContextError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'VisitorContextError';\n }\n\n static assert(parentType: VisitorContextType, childType: VisitorContextType) {\n if (\n (parentType === 'component' && childType !== 'region') ||\n (parentType === 'page' && childType !== 'region') ||\n (parentType === 'region' && childType !== 'component')\n ) {\n throw new VisitorContextError(\n `Invalid child context type ${childType} for parent context type ${parentType}`\n );\n }\n }\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { ShopperExperience } from '@/scapi-client/types';\nimport { VisitorContextError } from '../errors/visitor-context-error';\nimport type { InferNodeFromType, VisitorContextType } from '../types';\n\n/**\n * Context object passed to {@link PageVisitor} handler methods during page tree\n * traversal. Provides access to the current node via {@link node}, the tree\n * position via {@link page}, {@link parentRegion}, and {@link parentComponent},\n * and traversal methods ({@link visitRegions}, {@link visitComponents}) for\n * continuing into child nodes.\n *\n * When a visitor handler is defined, the handler is responsible for traversing\n * into children by calling the appropriate context method. If the handler does\n * not call these methods, children will not be visited.\n */\nexport class VisitorContext<TNode> {\n constructor(\n private readonly context: {\n /** The current node being visited. */\n node: TNode;\n /** The node type */\n type: VisitorContextType;\n /** The visitor being used to transform the page tree. */\n visitor: PageVisitor;\n /** The root page being traversed. */\n page?: ShopperExperience.schemas['Page'];\n /** The parent visitor context, providing access to the node that contains the current one in the page tree. */\n parent?: VisitorContext<\n | ShopperExperience.schemas['Page']\n | ShopperExperience.schemas['Region']\n | ShopperExperience.schemas['Component']\n >;\n /** The parent region of the current node, if traversing within a region. */\n parentRegion?: ShopperExperience.schemas['Region'];\n /** The parent component of the current node, if traversing within a component's nested regions. */\n parentComponent?: ShopperExperience.schemas['Component'];\n }\n ) {}\n\n get type(): VisitorContextType {\n return this.context.type;\n }\n\n /**\n * The current node being visited.\n */\n get node(): TNode {\n return this.context.node;\n }\n\n /**\n * The root page being traversed.\n */\n get page(): ShopperExperience.schemas['Page'] | undefined {\n return this.context.page;\n }\n\n /**\n * The parent visitor context, providing access to the node that contains the current one in the page tree.\n */\n get parent():\n | VisitorContext<\n | ShopperExperience.schemas['Page']\n | ShopperExperience.schemas['Region']\n | ShopperExperience.schemas['Component']\n >\n | undefined {\n return this.context.parent;\n }\n\n /**\n * The parent region of the current node, if traversing within a region.\n */\n get parentRegion(): ShopperExperience.schemas['Region'] | undefined {\n return this.context.parentRegion;\n }\n\n /**\n * The parent component of the current node, if traversing within a component's nested regions.\n */\n get parentComponent(): ShopperExperience.schemas['Component'] | undefined {\n return this.context.parentComponent;\n }\n\n /**\n * Traverses an array of regions, invoking the visitor's `visitRegion` handler\n * on each one. Regions for which the handler returns `null` are excluded from\n * the result. Call this from within a `visitPage` or `visitComponent` handler\n * to continue traversal into child regions.\n *\n * @param regions - The regions to traverse.\n * @returns The filtered array of transformed regions.\n *\n * @example\n * ```ts\n * transformPage(page, {\n * visitPage(context) {\n * // Traverse into regions explicitly\n * const regions = context.visitRegions(context.node.regions);\n * return { ...context.node, regions };\n * },\n * });\n * ```\n */\n visitRegions(regions: ShopperExperience.schemas['Region'][] = []): ShopperExperience.schemas['Region'][] {\n const newRegions = [];\n\n for (const region of regions) {\n const newRegion = this.visitRegion(region);\n\n if (newRegion) {\n newRegions.push(newRegion);\n }\n }\n\n return newRegions;\n }\n\n /**\n * Traverses a single region. If the visitor has a `visitRegion` handler, the\n * handler is called with a new {@link VisitorContext} for the region. Otherwise,\n * the region's child components are traversed automatically.\n *\n * @param region - The region to visit.\n * @returns The transformed region, or `null` to exclude it.\n */\n visitRegion(region: ShopperExperience.schemas['Region']): ShopperExperience.schemas['Region'] | null {\n const regionContext = this.toChildContext('region', region);\n\n if (this.context.visitor.visitRegion) {\n return this.context.visitor.visitRegion(regionContext);\n } else if (region.components) {\n return {\n ...region,\n components: regionContext.visitComponents(region.components),\n };\n }\n\n return region;\n }\n\n /**\n * Traverses an array of components, invoking the visitor's `visitComponent`\n * handler on each one. Components for which the handler returns `null` are\n * excluded from the result. Call this from within a `visitRegion` handler to\n * continue traversal into child components.\n *\n * @param components - The components to traverse.\n * @returns The filtered array of transformed components.\n *\n * @example\n * ```ts\n * transformPage(page, {\n * visitRegion(context) {\n * // Traverse into components explicitly\n * const components = context.visitComponents(context.node.components);\n * return { ...context.node, components };\n * },\n * });\n * ```\n */\n visitComponents(\n components: ShopperExperience.schemas['Component'][] = []\n ): ShopperExperience.schemas['Component'][] {\n const newComponents = [];\n\n for (const component of components) {\n const newComponent = this.visitComponent(component);\n\n if (newComponent) {\n newComponents.push(newComponent);\n }\n }\n\n return newComponents;\n }\n\n /**\n * Traverses a single component. If the visitor has a `visitComponent` handler,\n * the handler is called with a new {@link VisitorContext} for the component.\n * Otherwise, the component's nested regions are traversed automatically.\n *\n * @param component - The component to visit.\n * @returns The transformed component, or `null` to exclude it.\n */\n visitComponent(component: ShopperExperience.schemas['Component']): ShopperExperience.schemas['Component'] | null {\n const componentContext = this.toChildContext('component', component);\n\n if (this.context.visitor.visitComponent) {\n return this.context.visitor.visitComponent(componentContext);\n } else if (component.regions) {\n return {\n ...component,\n regions: componentContext.visitRegions(component.regions),\n };\n }\n\n return component;\n }\n\n /**\n * Traverses a single page. If the visitor has a `visitPage` handler, the\n * handler is called with a new {@link VisitorContext} for the page. Otherwise,\n * the page's regions are traversed automatically.\n *\n * @param page - The page to visit.\n * @returns The transformed page, or `null` to exclude it.\n */\n visitPage(page: ShopperExperience.schemas['Page']): ShopperExperience.schemas['Page'] | null {\n const pageContext = new VisitorContext({\n type: 'page',\n visitor: this.context.visitor,\n page,\n parentComponent: undefined,\n parentRegion: undefined,\n parent: undefined,\n node: page,\n });\n\n if (this.context.visitor.visitPage) {\n return this.context.visitor.visitPage(pageContext);\n } else if (page.regions) {\n const newPage = {\n ...page,\n regions: pageContext.visitRegions(page.regions),\n };\n\n return newPage;\n }\n\n return page;\n }\n\n private toChildContext<TType extends VisitorContextType>(\n type: TType,\n node: InferNodeFromType<TType>\n ): VisitorContext<InferNodeFromType<TType>> {\n VisitorContextError.assert(this.context.type, type);\n\n const parent = this as VisitorContext<\n | ShopperExperience.schemas['Region']\n | ShopperExperience.schemas['Component']\n | ShopperExperience.schemas['Page']\n >;\n\n if (type === 'region') {\n return new VisitorContext({\n type: 'region',\n visitor: this.context.visitor,\n page: this.page,\n node,\n parent,\n parentComponent: this.node as ShopperExperience.schemas['Component'],\n parentRegion: this.parentRegion,\n });\n }\n\n return new VisitorContext({\n type: 'component',\n visitor: this.context.visitor,\n page: this.page,\n node,\n parent,\n parentComponent: this.parentComponent,\n parentRegion: this.node as ShopperExperience.schemas['Region'],\n });\n }\n}\n\nclass RootVisitorContext extends VisitorContext<null> {\n constructor(visitor: PageVisitor) {\n super({\n node: null,\n type: 'root',\n visitor,\n });\n }\n}\n\n/**\n * Visitor interface for traversing and transforming a Page Designer page tree.\n * Implement any combination of visit methods to intercept pages, regions, or\n * components during traversal. Return `null` from `visitRegion` or\n * `visitComponent` to remove that element from the tree.\n */\nexport interface PageVisitor {\n visitPage?(context: VisitorContext<ShopperExperience.schemas['Page']>): ShopperExperience.schemas['Page'];\n visitRegion?(\n context: VisitorContext<ShopperExperience.schemas['Region']>\n ): ShopperExperience.schemas['Region'] | null;\n visitComponent?(\n component: VisitorContext<ShopperExperience.schemas['Component']>\n ): ShopperExperience.schemas['Component'] | null;\n}\n\n/**\n * Traverses a page tree using the visitor pattern, applying the visitor's\n * callbacks to the page, its regions, and their nested components. This is\n * the top-level entry point for page tree transformation.\n *\n * When a visitor handler is defined, it receives a {@link VisitorContext} and\n * is responsible for traversing into children using the context's traversal\n * methods (`visitRegions`, `visitComponents`). If the handler does not call\n * these methods, children will not be visited. When no handler is defined for\n * a node type, children are traversed automatically.\n *\n * Returning `null` from a `visitRegion` or `visitComponent` callback removes\n * that element and its children from the resulting tree.\n *\n * @param page - The page to traverse.\n * @param visitor - The visitor with callbacks to apply at each tree node.\n * @returns A new page with visitor transformations applied, or `null`.\n *\n * @example\n * ```ts\n * import { transformPage } from '@salesforce/storefront-next-runtime/design/data';\n *\n * const page = { id: 'homepage', typeId: 'storePage', regions: [\n * { id: 'header', components: [\n * { id: 'hero-banner', typeId: 'commerce_assets.heroBanner', regions: [] },\n * { id: 'promo-tile', typeId: 'commerce_assets.promoTile', regions: [] },\n * ]},\n * ]};\n *\n * // When only visitComponent is defined, regions are traversed automatically.\n * // The handler receives a VisitorContext — use context.node to access the component.\n * transformPage(page, {\n * visitComponent(context) {\n * console.log(`Component: ${context.node.typeId} in region ${context.parentRegion?.id}`);\n * return context.node;\n * },\n * });\n *\n * // When visitRegion is defined, the handler must traverse into children explicitly.\n * // Without calling context.visitComponents(), components inside the region are skipped.\n * transformPage(page, {\n * visitRegion(context) {\n * console.log(`Entering region: ${context.node.id}`);\n * const components = context.visitComponents(context.node.components);\n * return { ...context.node, components };\n * },\n * visitComponent(context) {\n * console.log(` Component: ${context.node.typeId}`);\n * return context.node;\n * },\n * });\n * ```\n */\nexport function transformPage(\n page: ShopperExperience.schemas['Page'],\n visitor: PageVisitor\n): ShopperExperience.schemas['Page'] | null {\n return new RootVisitorContext(visitor).visitPage(page);\n}\n\n/**\n * Applies the visitor to a single component. If the visitor's `visitComponent`\n * handler is defined, it receives a {@link VisitorContext} and is responsible\n * for traversing into the component's nested regions using `context.visitRegions()`.\n * If no `visitComponent` handler is defined, nested regions are traversed\n * automatically. Returns `null` to exclude the component from the result.\n *\n * @param component - The component to transform.\n * @param visitor - The visitor with callbacks.\n * @returns The transformed component, or `null` to exclude it.\n *\n * @example\n * ```ts\n * import { transformComponent } from '@salesforce/storefront-next-runtime/design/data';\n *\n * // Replace the image URL in a hero banner component and traverse its nested regions\n * const heroBanner = {\n * id: 'hero-1',\n * typeId: 'commerce_assets.heroBanner',\n * data: { imageUrl: '/images/summer-sale.jpg' },\n * regions: [{ id: 'banner-content', components: [] }],\n * };\n *\n * const result = transformComponent(heroBanner, {\n * visitComponent(context) {\n * // Traverse into nested regions using the context API\n * const regions = context.visitRegions(context.node.regions);\n *\n * if (context.node.typeId === 'commerce_assets.heroBanner') {\n * return { ...context.node, regions, data: { ...context.node.data, imageUrl: '/images/winter-sale.jpg' } };\n * }\n * return { ...context.node, regions };\n * },\n * });\n * ```\n */\nexport function transformComponent(\n component: ShopperExperience.schemas['Component'],\n visitor: PageVisitor\n): ShopperExperience.schemas['Component'] | null {\n return new RootVisitorContext(visitor).visitComponent(component);\n}\n\n/**\n * Applies the visitor to a single region. If the visitor's `visitRegion`\n * handler is defined, it receives a {@link VisitorContext} and is responsible\n * for traversing into the region's child components using `context.visitComponents()`.\n * If no `visitRegion` handler is defined, child components are traversed\n * automatically. Returns `null` to exclude the region and all its children\n * from the result.\n *\n * @param region - The region to transform.\n * @param visitor - The visitor with callbacks.\n * @returns The transformed region, or `null` to exclude it.\n *\n * @example\n * ```ts\n * import { transformRegion } from '@salesforce/storefront-next-runtime/design/data';\n *\n * // Filter empty regions and traverse into non-empty ones\n * const emptyRegion = { id: 'sidebar', components: [] };\n * const populatedRegion = { id: 'main', components: [\n * { id: 'product-grid', typeId: 'commerce_assets.productGrid', regions: [] },\n * ]};\n *\n * const visitor = {\n * visitRegion(context) {\n * if (!context.node.components?.length) {\n * return null; // Remove empty regions\n * }\n * // Traverse into child components using the context API\n * const components = context.visitComponents(context.node.components);\n * return { ...context.node, components };\n * },\n * };\n *\n * transformRegion(emptyRegion, visitor); // => null (removed)\n * transformRegion(populatedRegion, visitor); // => { id: 'main', components: [...] }\n * ```\n */\nexport function transformRegion(\n region: ShopperExperience.schemas['Region'],\n visitor: PageVisitor\n): ShopperExperience.schemas['Region'] | null {\n return new RootVisitorContext(visitor).visitRegion(region);\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { ComponentDataBinding, DataBindingRequirement, QualifierContext, ResolvedDataBinding } from '../types';\nimport type { ShopperExperience } from '@/scapi-client/types';\n\n/**\n * Pattern matching bare expressions: `type.field`.\n */\nconst BARE_EXPRESSION_PATTERN = /^(\\w+)\\.(\\w+)$/;\n\n/**\n * Parses a binding expression string into its provider type and field name.\n * Supports the bare `type.field` format.\n *\n * @param expression - The expression string to parse.\n * @returns The parsed type and field, or `null` if the expression is invalid.\n *\n * @example\n * ```ts\n * parseExpression('content_asset.title'); // { type: 'content_asset', field: 'title' }\n * parseExpression('invalid'); // null\n * ```\n */\nexport function parseExpression(expression: string): { type: string; field: string } | null {\n const match = expression.trim().match(BARE_EXPRESSION_PATTERN);\n if (match) {\n return { type: match[1], field: match[2] };\n }\n\n return null;\n}\n\n/**\n * Resolves a single binding expression against the component's data contexts\n * and the resolved data bindings from context resolution.\n *\n * Returns the resolved field value, or an empty string if the expression is\n * invalid, the matching context or record is not found, or the field does not\n * exist on the resolved record.\n *\n * @param expression - The expression string (e.g. `\"content_asset.body\"`).\n * @param contexts - The component's data binding contexts.\n * @param dataBindings - The resolved data bindings from {@link QualifierContext}.\n * @returns The resolved value, or `''` if resolution fails.\n */\nexport function resolveExpression(\n expression: string,\n contexts: DataBindingRequirement[],\n dataBindings: NonNullable<QualifierContext['dataBindings']>\n): unknown {\n const parsed = parseExpression(expression);\n if (!parsed) return '';\n\n const context = contexts.find((c) => c.type === parsed.type);\n if (!context) return '';\n\n const record: ResolvedDataBinding | undefined = dataBindings[context.type]?.[context.id];\n if (!record) return '';\n\n return record[parsed.field] ?? '';\n}\n\n/**\n * Resolves data binding expressions for a single component. Replaces attribute\n * values in the component's `data` with the resolved values from context\n * resolution. Attributes without a matching expression are preserved as-is.\n * When an expression cannot be resolved, the attribute value is set to an\n * empty string.\n *\n * Returns the component unchanged if it has no data binding metadata or if\n * `dataBindings` is `undefined`.\n *\n * @param component - The component to resolve data bindings for.\n * @param binding - The component's data binding metadata from the page manifest's `componentInfo`, or `null`/`undefined` if not bound.\n * @param dataBindings - The resolved data bindings from {@link QualifierContext}, or `undefined` if no bindings were resolved.\n * @returns The component with resolved attribute values, or the original component if no bindings apply.\n *\n * @example\n * ```ts\n * import { resolveComponentDataBindings } from '@salesforce/storefront-next-runtime/design/data';\n *\n * const component = {\n * id: 'banner',\n * typeId: 'commerce_assets.contentBanner',\n * data: { heading: 'Fallback Title', body: 'Fallback Body' },\n * regions: [],\n * };\n *\n * const binding = {\n * expressions: {\n * heading: 'content_asset.title',\n * body: 'content_asset.body',\n * },\n * contexts: [{ type: 'content_asset', id: 'winter-sale-uuid' }],\n * };\n *\n * const dataBindings = {\n * content_asset: {\n * 'winter-sale-uuid': {\n * title: 'Winter Sale',\n * body: '<div>Free Shipping on all orders!</div>',\n * },\n * },\n * };\n *\n * const resolved = resolveComponentDataBindings(component, binding, dataBindings);\n * // resolved.data.heading === 'Winter Sale'\n * // resolved.data.body === '<div>Free Shipping on all orders!</div>'\n * ```\n */\nexport function resolveComponentDataBindings(\n component: ShopperExperience.schemas['Component'],\n binding: ComponentDataBinding | null | undefined,\n dataBindings: QualifierContext['dataBindings']\n): ShopperExperience.schemas['Component'] {\n if (!dataBindings) {\n return component;\n }\n\n if (!binding?.contexts?.length) return component;\n\n const expressionEntries = Object.entries(binding.expressions ?? {});\n if (expressionEntries.length === 0) return component;\n\n const resolvedData: Record<string, unknown> = {\n ...(component.data as Record<string, unknown> | undefined),\n };\n\n for (const [attrName, expression] of expressionEntries) {\n resolvedData[attrName] = resolveExpression(expression, binding.contexts, dataBindings);\n }\n\n return {\n ...component,\n data: resolvedData as typeof component.data,\n };\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { QualifierContext, VisibilityRuleDef } from './types';\n\n/**\n * Evaluates a visibility rule against a shopper's qualifier context.\n *\n * Campaign-based and non-campaign rules are **mutually exclusive** paths,\n * matching the server's `VisibilityDefinition.isVisible()` logic:\n *\n * - **Campaign-based rule** (has `campaignQualifiers`): only the campaign\n * qualifiers are checked. Schedule, locale, and customer-group fields are\n * ignored because the campaign qualification already incorporates those\n * checks server-side.\n * - **Non-campaign rule**: locale, schedule, AND customer groups are checked.\n * All specified conditions must pass.\n *\n * When no context is provided and the rule requires campaign or customer group\n * checks, those checks will fail (returning `false`). Schedule checks do not\n * require context and are evaluated against `Date.now()`.\n *\n * @param rule - The visibility rule to evaluate.\n * @param locale - The current locale (e.g. `\"en_US\"`). Used to check whether the rule applies to this locale.\n * @param context - The shopper's active qualifiers, or `null`/`undefined` if not yet resolved.\n * @returns `true` if the rule's conditions pass, `false` otherwise.\n *\n * @example\n * ```ts\n * import { validateRule } from '@salesforce/storefront-next-runtime/design/data';\n *\n * // Campaign-based rule — only campaign qualifiers are evaluated\n * const campaignRule = {\n * activeLocales: ['en_US'],\n * campaignQualifiers: [{ campaignId: 'holiday-sale-2026', promotionId: 'free-shipping' }],\n * };\n *\n * // Non-campaign rule — locale, schedule AND customer groups are evaluated\n * const segmentRule = {\n * activeLocales: ['en_US', 'fr_FR'],\n * customerGroups: ['vip-customers'],\n * schedule: {\n * start: new Date('2026-12-01').toISOString(),\n * end: new Date('2026-12-31').toISOString(),\n * },\n * };\n * ```\n */\nexport function validateRule(rule: VisibilityRuleDef, locale: string, context?: QualifierContext | null): boolean {\n // Campaign-based rules and non-campaign rules are mutually exclusive\n // paths, mirroring the server's if/else-if branching.\n if (rule.campaignQualifiers) {\n for (const campaignQualifier of rule.campaignQualifiers) {\n if (!context?.campaignQualifiers?.[campaignQualifier.campaignId]?.[campaignQualifier.promotionId]) {\n return false;\n }\n }\n } else {\n if (rule.activeLocales && !rule.activeLocales.includes(locale)) {\n return false;\n }\n\n // Rule schedule times are in ISO 8601 format, so we need to convert them to milliseconds\n if (rule.schedule) {\n const now = Date.now();\n\n if (rule.schedule.start) {\n const startTimeInMillis = new Date(rule.schedule.start).getTime();\n\n // If the start time is invalid, the rule fails\n if (Number.isNaN(startTimeInMillis) || startTimeInMillis >= now) {\n return false;\n }\n }\n\n if (rule.schedule.end) {\n const endTimeInMillis = new Date(rule.schedule.end).getTime();\n\n // If the end time is invalid, the rule fails\n if (Number.isNaN(endTimeInMillis) || endTimeInMillis <= now) {\n return false;\n }\n }\n }\n\n if (rule.customerGroups) {\n for (const customerGroup of rule.customerGroups) {\n if (!context?.customerGroups?.[customerGroup]) {\n return false;\n }\n }\n }\n }\n\n return true;\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { transformPage } from './transform';\nimport { resolveComponentDataBindings } from './resolve-data-bindings';\nimport { validateRule } from '../validate-rule';\nimport type { QualifierContext, PageManifest, VariationEntry, RegionInfo } from '../types';\nimport type { ShopperExperience } from '@/scapi-client/types';\n\n/**\n * Context required for page processing. Contains the shopper's runtime\n * qualifiers, the component-level visibility rules, and the locale used\n * to resolve locale-specific component content from the page manifest.\n */\nexport interface PageProcessorContext {\n /** The shopper's active qualifiers (campaigns, customer groups), or `null` if not resolved. */\n qualifiers: QualifierContext | null;\n /** Component visibility rule definitions extracted from the page layout. */\n componentInfo: PageManifest['componentInfo'];\n /** Page-level region configuration (e.g. maxComponents limits) for top-level regions not nested under a component. */\n pageInfo: {\n regions: VariationEntry['regions'];\n };\n /** The locale to use when resolving locale-specific component content (e.g. `\"en_US\"`). */\n locale: string;\n /**\n * When `true` (default), invisible components are removed from the tree and\n * regions are truncated to their `maxComponents` limit. When `false`, invisible\n * components and overflow components are kept in the tree but marked with\n * `visible: false` — used in design/preview mode so the editor can display them.\n */\n pruneInvisible?: boolean;\n}\n\n/**\n * Filters a page's components based on their visibility rules and resolves\n * data binding expressions in a single traversal. Traverses the page tree\n * using the visitor pattern and:\n *\n * 1. Removes any component whose visibility rules do not pass against the\n * shopper's qualifier context.\n * 2. Resolves data binding expressions in each surviving component's `data`\n * attributes using the resolved data bindings from context resolution.\n *\n * A component is visible if **any** of its visibility rules pass (OR logic).\n * If a component has rules and none of them pass, it is removed. Components\n * without rules are always included.\n *\n * @param page - The page to process.\n * @param context - The processing context with qualifier data, visibility rules, and resolved data bindings.\n * @returns A new page with invisible components filtered out and data binding expressions resolved.\n *\n * @example\n * ```ts\n * import { processPage } from '@salesforce/storefront-next-runtime/design/data';\n *\n * const page = {\n * id: 'homepage',\n * typeId: 'storePage',\n * regions: [{\n * id: 'main',\n * components: [\n * { id: 'public-banner', typeId: 'commerce_assets.heroBanner', regions: [] },\n * { id: 'loyalty-offer', typeId: 'commerce_assets.promoTile', regions: [] },\n * ],\n * }],\n * };\n *\n * // The \"loyalty-offer\" component requires the shopper to be in \"loyalty-members\"\n * const componentInfo = {\n * 'public-banner': { visibilityRules: [] },\n * 'loyalty-offer': {\n * visibilityRules: [{ customerGroups: ['loyalty-members'] }],\n * },\n * };\n *\n * // Guest shopper — not in any customer group\n * const filtered = processPage(page, {\n * qualifiers: { customerGroups: {}, campaignQualifiers: {} },\n * componentInfo,\n * });\n * // filtered.regions[0].components has only \"public-banner\"\n * // \"loyalty-offer\" was removed because the shopper isn't a loyalty member\n * ```\n */\nexport function processPage(\n page: ShopperExperience.schemas['Page'],\n processorContext: PageProcessorContext\n): ShopperExperience.schemas['Page'] {\n const { pruneInvisible = true } = processorContext;\n\n return transformPage(page, {\n visitRegion(ctx) {\n let regionInfo: RegionInfo | undefined;\n\n if (ctx.parent?.type === 'page') {\n regionInfo = processorContext.pageInfo.regions[ctx.node.id];\n } else if (ctx.parent?.type === 'component') {\n regionInfo = processorContext.componentInfo[ctx.parent.node.id]?.regions?.[ctx.node.id];\n }\n\n // Visit each component first — this runs visitComponent which\n // filters out components that fail their visibility rules.\n let components = ctx.visitComponents(ctx.node.components);\n\n if (regionInfo?.maxComponents != null) {\n if (pruneInvisible) {\n components = components.slice(0, regionInfo.maxComponents);\n } else {\n const result: ShopperExperience.schemas['Component'][] = [];\n let visibleCount = 0;\n\n for (const comp of components) {\n if (comp.visible) {\n visibleCount++;\n }\n\n if (visibleCount > regionInfo.maxComponents) {\n result.push({ ...comp, visible: false });\n } else {\n result.push(comp);\n }\n }\n\n components = result;\n }\n }\n\n return {\n ...ctx.node,\n // After visibility filtering, enforce the region's max component\n // limit by keeping only the first N visible components.\n components,\n };\n },\n visitComponent(ctx) {\n const componentInfo = processorContext.componentInfo[ctx.node.id];\n const visibilityRules = componentInfo?.visibilityRules ?? [];\n let isVisible = true;\n\n // Visibility rules use OR logic: the component is visible\n // if ANY rule passes. Only remove it when it has its own\n // rules and none of them pass.\n if (visibilityRules.length > 0) {\n const anyRulePassed = visibilityRules.some((rule) =>\n validateRule(rule, processorContext.locale, processorContext.qualifiers)\n );\n\n if (!anyRulePassed) {\n if (pruneInvisible) {\n return null;\n }\n\n isVisible = false;\n }\n }\n\n // Apply locale-specific content from the manifest to the component's data.\n // The \"default\" locale provides base values; the current locale overrides them.\n const defaultContent = componentInfo?.content?.default ?? {};\n const localeContent = componentInfo?.content?.[processorContext.locale] ?? {};\n const content = { ...defaultContent, ...localeContent };\n const isLocalized = Boolean(componentInfo?.content?.[processorContext.locale]);\n\n let node: ShopperExperience.schemas['Component'] = {\n ...ctx.node,\n localized: isLocalized,\n visible: isVisible,\n data: {\n ...(ctx.node.data as Record<string, unknown>),\n ...content,\n } as typeof ctx.node.data,\n };\n\n // Resolve data binding expressions (overrides content for bound attributes).\n node = resolveComponentDataBindings(\n node,\n componentInfo?.dataBinding,\n processorContext.qualifiers?.dataBindings\n );\n\n return {\n ...node,\n regions: ctx.visitRegions(ctx.node.regions),\n };\n },\n }) as ShopperExperience.schemas['Page'];\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nexport class RequiredError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'RequiredError';\n }\n\n static assert<TValue>(\n value: TValue,\n message: string,\n isEmpty: (value: TValue) => boolean = (v) => v == null\n ): asserts value is NonNullable<TValue> {\n if (isEmpty(value)) {\n throw new RequiredError(message);\n }\n }\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { SiteManifest } from '../types';\n\n/**\n * The result of resolving an identifier through a content assignment resolver.\n * Contains the object type, aspect type, and ordered list of keys to search\n * in the site manifest's content assignments.\n */\nexport interface ResolvedContentAssignmentLookup {\n /** The type of commerce object (e.g. `'product'`, `'category'`). */\n objectType: string;\n /** Ordered list of object IDs to search in the site manifest's content assignments. */\n keys: string[];\n}\n\n/**\n * A function that converts an identifier key (e.g., a product or category ID)\n * into a {@link ResolvedContentAssignmentLookup} describing where to search\n * in the site manifest for the assigned page ID.\n */\nexport type ContentAssignmentResolver = (\n key: string,\n manifest?: SiteManifest | null\n) => ResolvedContentAssignmentLookup;\n\n/**\n * Registry of content assignment resolvers keyed by {@link IdentifierType}.\n * Each resolver knows how to convert its identifier type into a set of lookup\n * keys for the site manifest.\n *\n * Built-in resolvers:\n * - **`'product'`** — Maps a product ID to a single PDP lookup key.\n * - **`'category'`** — Maps a category ID to an ordered list of keys that\n * traverses the category hierarchy from child to root, enabling inherited\n * page assignments.\n *\n * The `'page'` identifier type has no resolver — page IDs are used directly.\n *\n * @example\n * ```ts\n * import { ContentAssignmentResolvers } from '@salesforce/storefront-next-runtime/design/data';\n *\n * // Resolve a product identifier for PDP lookup\n * const productResolver = ContentAssignmentResolvers.get('product');\n * productResolver('nike-air-max-90');\n * // => { objectType: 'product', aspectType: 'pdp', keys: ['nike-air-max-90'] }\n *\n * // Resolve a category identifier — traverses hierarchy to find inherited assignments\n * const categoryResolver = ContentAssignmentResolvers.get('category');\n * const siteManifest = {\n * categories: {\n * 'mens-running-shoes': { name: 'Running Shoes', parentCategory: 'mens-shoes' },\n * 'mens-shoes': { name: \"Men's Shoes\", parentCategory: 'mens' },\n * 'mens': { name: 'Men' },\n * },\n * contentObjectAssignments: {},\n * };\n * categoryResolver('mens-running-shoes', siteManifest);\n * // => { objectType: 'category', aspectType: 'plp', keys: ['mens-running-shoes', 'mens-shoes', 'mens'] }\n * ```\n */\nexport const ContentAssignmentResolvers = new Map<string, ContentAssignmentResolver>([\n [\n 'product',\n (key) => ({\n objectType: 'product',\n keys: [key],\n }),\n ],\n [\n 'category',\n (key, manifest) => {\n const keys = [];\n const visited = new Set<string>();\n\n let currentCategoryId: string | undefined = key;\n\n while (currentCategoryId && !visited.has(currentCategoryId)) {\n visited.add(currentCategoryId);\n keys.push(currentCategoryId);\n currentCategoryId = manifest?.categories[currentCategoryId]?.parentCategory;\n }\n\n return {\n objectType: 'category',\n keys,\n };\n },\n ],\n]);\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { SiteManifest, IdentifierType } from '../types';\nimport { ContentAssignmentResolvers } from './content-assignment-resolvers';\n\n/**\n * Converts a product or category identifier into a page ID by looking up\n * content assignments in the site manifest. For categories, the lookup\n * traverses the category hierarchy from the given category up to the root,\n * returning the first matching assignment.\n *\n * Returns `null` if no content assignment is found for the identifier or if\n * the identifier type has no registered resolver.\n *\n * @param options - The resolution options.\n * @param options.id - The identifier to resolve (product ID, category ID, or page ID).\n * @param options.identifierType - The type of identifier: `'product'`, `'category'`, or `'page'`.\n * @param options.siteManifest - The site manifest containing content assignments and category hierarchy.\n * @returns The resolved page ID, or `null` if no assignment was found.\n *\n * @example\n * ```ts\n * import { resolveDynamicPageId } from '@salesforce/storefront-next-runtime/design/data';\n *\n * const siteManifest = {\n * contentObjectAssignments: {\n * plp: {\n * category: {\n * 'mens-shoes': {\n * lookupMode: 'category-explicit',\n * contentId: 'page-mens-shoes-plp',\n * },\n * },\n * },\n * },\n * categories: {\n * 'mens-running-shoes': { name: 'Running Shoes', parentCategory: 'mens-shoes' },\n * 'mens-shoes': { name: \"Men's Shoes\" },\n * },\n * };\n *\n * // Direct match\n * resolveDynamicPageId({ id: 'mens-shoes', identifierType: 'category', siteManifest });\n * // => 'page-mens-shoes-plp'\n *\n * // Inherited from parent category\n * resolveDynamicPageId({ id: 'mens-running-shoes', identifierType: 'category', siteManifest });\n * // => 'page-mens-shoes-plp' (found via parent traversal)\n *\n * // No assignment found\n * resolveDynamicPageId({ id: 'womens-shoes', identifierType: 'category', siteManifest });\n * // => null\n * ```\n */\nexport function resolveDynamicPageId<TIdentifier extends IdentifierType = IdentifierType>({\n id,\n identifierType,\n siteManifest,\n aspectType,\n}: {\n id: string;\n identifierType: TIdentifier;\n aspectType: string;\n siteManifest?: SiteManifest | null;\n}): string | null {\n const resolvedContentAssignmentLookup = ContentAssignmentResolvers.get(identifierType)?.(id, siteManifest);\n\n if (resolvedContentAssignmentLookup) {\n for (const key of resolvedContentAssignmentLookup.keys) {\n const contentAssignment =\n siteManifest?.contentObjectAssignments?.[aspectType]?.[resolvedContentAssignmentLookup.objectType]?.[\n key\n ];\n\n if (contentAssignment) {\n return contentAssignment.contentId;\n }\n }\n }\n\n return null;\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { ContextResolver, PageManifest, QualifierContext, VariationEntry } from '../types';\nimport { validateRule } from '../validate-rule';\n\n/**\n * Selects the appropriate page variation from a manifest by evaluating each\n * variation's visibility rule in order. Returns the first variation whose rule\n * passes, or falls back to the manifest's default variation.\n *\n * The qualifier context is resolved lazily — the `contextResolver` is only\n * called when a variation's `ruleRequiresContext` flag is `true`, and only\n * once (the result is cached for subsequent variations).\n *\n * @param manifest - The page manifest containing all variations.\n * @param options - Resolution options.\n * @param options.contextResolver - Optional async function that returns the shopper's qualifier context. Only called if a variation's rule needs it.\n * @param options.locale - The current locale (e.g. `\"en_US\"`). Used to evaluate locale-based visibility rules.\n * @returns The selected variation entry and resolved context, or `null` if no variation (including default) exists.\n *\n * @example\n * ```ts\n * import { getPageFromManifest } from '@salesforce/storefront-next-runtime/design/data';\n *\n * const manifest = {\n * pageId: 'homepage',\n * context: { campaignQualifiers: [], customerGroups: ['vip-customers'], dataBindings: [] },\n * variationOrder: ['vip-homepage', 'holiday-homepage'],\n * variations: {\n * 'vip-homepage': {\n * ruleRequiresContext: true,\n * pageRequiresContext: false,\n * visibilityRule: { activeLocales: ['en-US'], customerGroups: ['vip-customers'] },\n * page: { id: 'homepage', typeId: 'storePage', regions: [] },\n * regions: {},\n * },\n * 'holiday-homepage': {\n * ruleRequiresContext: false,\n * pageRequiresContext: false,\n * visibilityRule: {\n * activeLocales: ['en-US'],\n * schedule: {\n * start: new Date('2026-12-01').toISOString(),\n * end: new Date('2026-12-31').toISOString(),\n * },\n * },\n * page: { id: 'homepage', typeId: 'storePage', regions: [] },\n * regions: {},\n * },\n * 'default-homepage': {\n * ruleRequiresContext: false,\n * pageRequiresContext: false,\n * page: { id: 'homepage', typeId: 'storePage', regions: [] },\n * regions: {},\n * },\n * },\n * defaultVariation: 'default-homepage',\n * componentInfo: {},\n * };\n *\n * // VIP shopper — matches first variation\n * const result = await getPageFromManifest(manifest, {\n * locale: 'en-US',\n * contextResolver: async () => ({\n * customerGroups: { 'vip-customers': true },\n * campaignQualifiers: {},\n * }),\n * });\n * // result.entry === manifest.variations['vip-homepage']\n *\n * // Non-VIP shopper outside holiday window — falls back to default\n * const fallback = await getPageFromManifest(manifest, {\n * locale: 'en-US',\n * contextResolver: async () => ({\n * customerGroups: {},\n * campaignQualifiers: {},\n * }),\n * });\n * // fallback.entry === manifest.variations['default-homepage']\n * ```\n */\nexport async function getPageFromManifest(\n manifest: PageManifest,\n {\n contextResolver,\n locale,\n }: {\n contextResolver?: ContextResolver;\n locale: string;\n }\n): Promise<{\n entry: VariationEntry;\n context: QualifierContext | null;\n} | null> {\n let context: QualifierContext | null = null;\n let resolvedVariation: VariationEntry | null = null;\n\n for (const variationId of manifest.variationOrder) {\n const variation = manifest.variations[variationId];\n\n if (variation?.ruleRequiresContext && !context) {\n context = (await contextResolver?.(manifest.context)) ?? null;\n }\n\n if (!variation?.visibilityRule || validateRule(variation.visibilityRule, locale, context)) {\n resolvedVariation = variation;\n break;\n }\n }\n\n if (!resolvedVariation) {\n resolvedVariation = manifest.variations[manifest.defaultVariation];\n }\n\n if (!resolvedVariation) {\n return null;\n }\n\n return {\n entry: resolvedVariation,\n context,\n };\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { IdentifierType, ManifestStorage, ContextResolver, QualifierContext } from '../types';\nimport type { ShopperExperience } from '@/scapi-client/types';\nimport { ContentAssignmentResolvers } from '../manifest/content-assignment-resolvers';\nimport { resolveDynamicPageId } from '../manifest/resolve-dynamic-page-id';\nimport { getPageFromManifest } from '../manifest/get-page';\nimport { processPage } from './process-page';\nimport { RequiredError } from '../errors/required';\n\n/**\n * Main entry point for the page resolution pipeline. Orchestrates the full flow:\n *\n * 1. **Resolve dynamic page ID** — For product/category identifiers, looks up\n * the assigned page ID via content assignments in the site manifest.\n * 2. **Fetch page manifest** — Loads all variations for the resolved page.\n * 3. **Select variation** — Evaluates visibility rules to pick the right variation.\n * 4. **Load qualifier context** — Lazily fetches the shopper's context only if needed.\n * 5. **Process page** — Filters out components that fail visibility rules.\n *\n * Returns `null` if the page ID cannot be resolved, the manifest doesn't exist,\n * or no variation is available.\n *\n * @param options - The resolution options.\n * @param options.id - The identifier to resolve (product ID, category ID, or page ID).\n * @param options.identifierType - The type of identifier: `'product'`, `'category'`, or `'page'`.\n * @param options.locale - The locale to resolve the page for (e.g. `\"en-US\"`).\n * @param options.manifestStorage - Storage implementation for fetching manifests.\n * @param options.contextResolver - Optional async function that returns the shopper's qualifier context. Only called if a visibility rule needs it.\n * @param options.aspectType - The aspect type to resolve the page for when the identifier type is `'product'` or `'category'`.\n * @param options.pruneInvisible - When `true` (default), invisible and overflow components are removed. When `false`, they are kept but marked `visible: false` for design/preview mode.\n * @returns The fully resolved and filtered page, or `null`.\n *\n * @example\n * ```ts\n * import { resolvePage } from '@salesforce/storefront-next-runtime/design/data';\n *\n * // Resolve the PDP page for a specific product with an active holiday campaign\n * const page = await resolvePage({\n * id: 'nike-air-max-90',\n * identifierType: 'product',\n * aspectType: 'pdp',\n * locale: 'en-US',\n * manifestStorage: {\n * async getPageManifest(id) {\n * // Fetch from CDN, filesystem, or database\n * return fetchManifest(`/manifests/${id}.json`);\n * },\n * async getSiteManifest() {\n * return fetchManifest('/manifests/site.json');\n * },\n * },\n * contextResolver: async () => ({\n * customerGroups: { 'vip-customers': true },\n * campaignQualifiers: {\n * 'holiday-sale-2026': { 'free-shipping': true },\n * },\n * }),\n * });\n *\n * if (page) {\n * // page.regions contains only components visible to this VIP shopper\n * // during the holiday sale campaign\n * renderPage(page);\n * }\n * ```\n */\nexport async function resolvePage({\n id,\n identifierType,\n aspectType,\n locale,\n manifestStorage,\n contextResolver,\n pruneInvisible = true,\n}: {\n id: string;\n identifierType: IdentifierType;\n aspectType?: string;\n locale: string;\n manifestStorage: ManifestStorage;\n contextResolver?: ContextResolver;\n pruneInvisible?: boolean;\n}): Promise<ShopperExperience.schemas['Page'] | null> {\n let resolvedId: string | null = null;\n\n if (ContentAssignmentResolvers.has(identifierType)) {\n const siteManifest = await manifestStorage.getSiteManifest();\n\n RequiredError.assert(aspectType, `Aspect type is required for identifier type ${identifierType}`, (v) => !v);\n\n resolvedId = resolveDynamicPageId({ id, identifierType, aspectType, siteManifest });\n } else {\n resolvedId = id;\n }\n\n if (!resolvedId) {\n return null;\n }\n\n const pageManifest = await manifestStorage.getPageManifest(resolvedId);\n\n if (!pageManifest) {\n return null;\n }\n\n const pageResults = await getPageFromManifest(pageManifest, {\n contextResolver,\n locale,\n });\n\n if (!pageResults) {\n return null;\n }\n\n let context: QualifierContext | null = null;\n\n if (pageResults.entry.pageRequiresContext) {\n context = pageResults.context ?? (await contextResolver?.(pageManifest.context)) ?? null;\n }\n\n return processPage(pageResults.entry.page, {\n qualifiers: context,\n componentInfo: pageManifest.componentInfo,\n pageInfo: {\n regions: pageResults.entry.regions,\n },\n locale,\n pruneInvisible,\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"design-data.js","names":["context: {\n /** The current node being visited. */\n node: TNode;\n /** The node type */\n type: VisitorContextType;\n /** The visitor being used to transform the page tree. */\n visitor: PageVisitor;\n /** The root page being traversed. */\n page?: ShopperExperience.schemas['Page'];\n /** The parent visitor context, providing access to the node that contains the current one in the page tree. */\n parent?: VisitorContext<\n | ShopperExperience.schemas['Page']\n | ShopperExperience.schemas['Region']\n | ShopperExperience.schemas['Component']\n >;\n /** The parent region of the current node, if traversing within a region. */\n parentRegion?: ShopperExperience.schemas['Region'];\n /** The parent component of the current node, if traversing within a component's nested regions. */\n parentComponent?: ShopperExperience.schemas['Component'];\n }","record: ResolvedDataBinding | undefined","resolvedData: Record<string, unknown>","out: ResolvedImage","out: Record<string, unknown>","result: Record<string, unknown>","result: ShopperExperience.schemas['Page']","regionInfo: RegionInfo | undefined","result: ShopperExperience.schemas['Component'][]","node: ShopperExperience.schemas['Component']","currentCategoryId: string | undefined","context: QualifierContext | null","resolvedVariation: VariationEntry | null","out: ShopperExperience.schemas['Page']","resolvedId: string | null","context: QualifierContext | null"],"sources":["../src/design/data/errors/visitor-context-error.ts","../src/design/data/page/transform.ts","../src/design/data/page/resolve-data-bindings.ts","../src/design/data/page/markup-url-rewriter.ts","../src/design/data/page/attribute-resolution.ts","../src/design/data/validate-rule.ts","../src/design/data/page/process-page.ts","../src/design/data/errors/required.ts","../src/design/data/manifest/content-assignment-resolvers.ts","../src/design/data/manifest/resolve-dynamic-page-id.ts","../src/design/data/manifest/get-page.ts","../src/design/data/page/resolve-page.ts"],"sourcesContent":["/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { VisitorContextType } from '../types';\n\nexport class VisitorContextError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'VisitorContextError';\n }\n\n static assert(parentType: VisitorContextType, childType: VisitorContextType) {\n if (\n (parentType === 'component' && childType !== 'region') ||\n (parentType === 'page' && childType !== 'region') ||\n (parentType === 'region' && childType !== 'component')\n ) {\n throw new VisitorContextError(\n `Invalid child context type ${childType} for parent context type ${parentType}`\n );\n }\n }\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { ShopperExperience } from '@/scapi-client/types';\nimport { VisitorContextError } from '../errors/visitor-context-error';\nimport type { InferNodeFromType, VisitorContextType } from '../types';\n\n/**\n * Context object passed to {@link PageVisitor} handler methods during page tree\n * traversal. Provides access to the current node via {@link node}, the tree\n * position via {@link page}, {@link parentRegion}, and {@link parentComponent},\n * and traversal methods ({@link visitRegions}, {@link visitComponents}) for\n * continuing into child nodes.\n *\n * When a visitor handler is defined, the handler is responsible for traversing\n * into children by calling the appropriate context method. If the handler does\n * not call these methods, children will not be visited.\n */\nexport class VisitorContext<TNode> {\n constructor(\n private readonly context: {\n /** The current node being visited. */\n node: TNode;\n /** The node type */\n type: VisitorContextType;\n /** The visitor being used to transform the page tree. */\n visitor: PageVisitor;\n /** The root page being traversed. */\n page?: ShopperExperience.schemas['Page'];\n /** The parent visitor context, providing access to the node that contains the current one in the page tree. */\n parent?: VisitorContext<\n | ShopperExperience.schemas['Page']\n | ShopperExperience.schemas['Region']\n | ShopperExperience.schemas['Component']\n >;\n /** The parent region of the current node, if traversing within a region. */\n parentRegion?: ShopperExperience.schemas['Region'];\n /** The parent component of the current node, if traversing within a component's nested regions. */\n parentComponent?: ShopperExperience.schemas['Component'];\n }\n ) {}\n\n get type(): VisitorContextType {\n return this.context.type;\n }\n\n /**\n * The current node being visited.\n */\n get node(): TNode {\n return this.context.node;\n }\n\n /**\n * The root page being traversed.\n */\n get page(): ShopperExperience.schemas['Page'] | undefined {\n return this.context.page;\n }\n\n /**\n * The parent visitor context, providing access to the node that contains the current one in the page tree.\n */\n get parent():\n | VisitorContext<\n | ShopperExperience.schemas['Page']\n | ShopperExperience.schemas['Region']\n | ShopperExperience.schemas['Component']\n >\n | undefined {\n return this.context.parent;\n }\n\n /**\n * The parent region of the current node, if traversing within a region.\n */\n get parentRegion(): ShopperExperience.schemas['Region'] | undefined {\n return this.context.parentRegion;\n }\n\n /**\n * The parent component of the current node, if traversing within a component's nested regions.\n */\n get parentComponent(): ShopperExperience.schemas['Component'] | undefined {\n return this.context.parentComponent;\n }\n\n /**\n * Traverses an array of regions, invoking the visitor's `visitRegion` handler\n * on each one. Regions for which the handler returns `null` are excluded from\n * the result. Call this from within a `visitPage` or `visitComponent` handler\n * to continue traversal into child regions.\n *\n * @param regions - The regions to traverse.\n * @returns The filtered array of transformed regions.\n *\n * @example\n * ```ts\n * transformPage(page, {\n * visitPage(context) {\n * // Traverse into regions explicitly\n * const regions = context.visitRegions(context.node.regions);\n * return { ...context.node, regions };\n * },\n * });\n * ```\n */\n visitRegions(regions: ShopperExperience.schemas['Region'][] = []): ShopperExperience.schemas['Region'][] {\n const newRegions = [];\n\n for (const region of regions) {\n const newRegion = this.visitRegion(region);\n\n if (newRegion) {\n newRegions.push(newRegion);\n }\n }\n\n return newRegions;\n }\n\n /**\n * Traverses a single region. If the visitor has a `visitRegion` handler, the\n * handler is called with a new {@link VisitorContext} for the region. Otherwise,\n * the region's child components are traversed automatically.\n *\n * @param region - The region to visit.\n * @returns The transformed region, or `null` to exclude it.\n */\n visitRegion(region: ShopperExperience.schemas['Region']): ShopperExperience.schemas['Region'] | null {\n const regionContext = this.toChildContext('region', region);\n\n if (this.context.visitor.visitRegion) {\n return this.context.visitor.visitRegion(regionContext);\n } else if (region.components) {\n return {\n ...region,\n components: regionContext.visitComponents(region.components),\n };\n }\n\n return region;\n }\n\n /**\n * Traverses an array of components, invoking the visitor's `visitComponent`\n * handler on each one. Components for which the handler returns `null` are\n * excluded from the result. Call this from within a `visitRegion` handler to\n * continue traversal into child components.\n *\n * @param components - The components to traverse.\n * @returns The filtered array of transformed components.\n *\n * @example\n * ```ts\n * transformPage(page, {\n * visitRegion(context) {\n * // Traverse into components explicitly\n * const components = context.visitComponents(context.node.components);\n * return { ...context.node, components };\n * },\n * });\n * ```\n */\n visitComponents(\n components: ShopperExperience.schemas['Component'][] = []\n ): ShopperExperience.schemas['Component'][] {\n const newComponents = [];\n\n for (const component of components) {\n const newComponent = this.visitComponent(component);\n\n if (newComponent) {\n newComponents.push(newComponent);\n }\n }\n\n return newComponents;\n }\n\n /**\n * Traverses a single component. If the visitor has a `visitComponent` handler,\n * the handler is called with a new {@link VisitorContext} for the component.\n * Otherwise, the component's nested regions are traversed automatically.\n *\n * @param component - The component to visit.\n * @returns The transformed component, or `null` to exclude it.\n */\n visitComponent(component: ShopperExperience.schemas['Component']): ShopperExperience.schemas['Component'] | null {\n const componentContext = this.toChildContext('component', component);\n\n if (this.context.visitor.visitComponent) {\n return this.context.visitor.visitComponent(componentContext);\n } else if (component.regions) {\n return {\n ...component,\n regions: componentContext.visitRegions(component.regions),\n };\n }\n\n return component;\n }\n\n /**\n * Traverses a single page. If the visitor has a `visitPage` handler, the\n * handler is called with a new {@link VisitorContext} for the page. Otherwise,\n * the page's regions are traversed automatically.\n *\n * @param page - The page to visit.\n * @returns The transformed page, or `null` to exclude it.\n */\n visitPage(page: ShopperExperience.schemas['Page']): ShopperExperience.schemas['Page'] | null {\n const pageContext = new VisitorContext({\n type: 'page',\n visitor: this.context.visitor,\n page,\n parentComponent: undefined,\n parentRegion: undefined,\n parent: undefined,\n node: page,\n });\n\n if (this.context.visitor.visitPage) {\n return this.context.visitor.visitPage(pageContext);\n } else if (page.regions) {\n const newPage = {\n ...page,\n regions: pageContext.visitRegions(page.regions),\n };\n\n return newPage;\n }\n\n return page;\n }\n\n private toChildContext<TType extends VisitorContextType>(\n type: TType,\n node: InferNodeFromType<TType>\n ): VisitorContext<InferNodeFromType<TType>> {\n VisitorContextError.assert(this.context.type, type);\n\n const parent = this as VisitorContext<\n | ShopperExperience.schemas['Region']\n | ShopperExperience.schemas['Component']\n | ShopperExperience.schemas['Page']\n >;\n\n if (type === 'region') {\n return new VisitorContext({\n type: 'region',\n visitor: this.context.visitor,\n page: this.page,\n node,\n parent,\n parentComponent: this.node as ShopperExperience.schemas['Component'],\n parentRegion: this.parentRegion,\n });\n }\n\n return new VisitorContext({\n type: 'component',\n visitor: this.context.visitor,\n page: this.page,\n node,\n parent,\n parentComponent: this.parentComponent,\n parentRegion: this.node as ShopperExperience.schemas['Region'],\n });\n }\n}\n\nclass RootVisitorContext extends VisitorContext<null> {\n constructor(visitor: PageVisitor) {\n super({\n node: null,\n type: 'root',\n visitor,\n });\n }\n}\n\n/**\n * Visitor interface for traversing and transforming a Page Designer page tree.\n * Implement any combination of visit methods to intercept pages, regions, or\n * components during traversal. Return `null` from `visitRegion` or\n * `visitComponent` to remove that element from the tree.\n */\nexport interface PageVisitor {\n visitPage?(context: VisitorContext<ShopperExperience.schemas['Page']>): ShopperExperience.schemas['Page'];\n visitRegion?(\n context: VisitorContext<ShopperExperience.schemas['Region']>\n ): ShopperExperience.schemas['Region'] | null;\n visitComponent?(\n component: VisitorContext<ShopperExperience.schemas['Component']>\n ): ShopperExperience.schemas['Component'] | null;\n}\n\n/**\n * Traverses a page tree using the visitor pattern, applying the visitor's\n * callbacks to the page, its regions, and their nested components. This is\n * the top-level entry point for page tree transformation.\n *\n * When a visitor handler is defined, it receives a {@link VisitorContext} and\n * is responsible for traversing into children using the context's traversal\n * methods (`visitRegions`, `visitComponents`). If the handler does not call\n * these methods, children will not be visited. When no handler is defined for\n * a node type, children are traversed automatically.\n *\n * Returning `null` from a `visitRegion` or `visitComponent` callback removes\n * that element and its children from the resulting tree.\n *\n * @param page - The page to traverse.\n * @param visitor - The visitor with callbacks to apply at each tree node.\n * @returns A new page with visitor transformations applied, or `null`.\n *\n * @example\n * ```ts\n * import { transformPage } from '@salesforce/storefront-next-runtime/design/data';\n *\n * const page = { id: 'homepage', typeId: 'storePage', regions: [\n * { id: 'header', components: [\n * { id: 'hero-banner', typeId: 'commerce_assets.heroBanner', regions: [] },\n * { id: 'promo-tile', typeId: 'commerce_assets.promoTile', regions: [] },\n * ]},\n * ]};\n *\n * // When only visitComponent is defined, regions are traversed automatically.\n * // The handler receives a VisitorContext — use context.node to access the component.\n * transformPage(page, {\n * visitComponent(context) {\n * console.log(`Component: ${context.node.typeId} in region ${context.parentRegion?.id}`);\n * return context.node;\n * },\n * });\n *\n * // When visitRegion is defined, the handler must traverse into children explicitly.\n * // Without calling context.visitComponents(), components inside the region are skipped.\n * transformPage(page, {\n * visitRegion(context) {\n * console.log(`Entering region: ${context.node.id}`);\n * const components = context.visitComponents(context.node.components);\n * return { ...context.node, components };\n * },\n * visitComponent(context) {\n * console.log(` Component: ${context.node.typeId}`);\n * return context.node;\n * },\n * });\n * ```\n */\nexport function transformPage(\n page: ShopperExperience.schemas['Page'],\n visitor: PageVisitor\n): ShopperExperience.schemas['Page'] | null {\n return new RootVisitorContext(visitor).visitPage(page);\n}\n\n/**\n * Applies the visitor to a single component. If the visitor's `visitComponent`\n * handler is defined, it receives a {@link VisitorContext} and is responsible\n * for traversing into the component's nested regions using `context.visitRegions()`.\n * If no `visitComponent` handler is defined, nested regions are traversed\n * automatically. Returns `null` to exclude the component from the result.\n *\n * @param component - The component to transform.\n * @param visitor - The visitor with callbacks.\n * @returns The transformed component, or `null` to exclude it.\n *\n * @example\n * ```ts\n * import { transformComponent } from '@salesforce/storefront-next-runtime/design/data';\n *\n * // Replace the image URL in a hero banner component and traverse its nested regions\n * const heroBanner = {\n * id: 'hero-1',\n * typeId: 'commerce_assets.heroBanner',\n * data: { imageUrl: '/images/summer-sale.jpg' },\n * regions: [{ id: 'banner-content', components: [] }],\n * };\n *\n * const result = transformComponent(heroBanner, {\n * visitComponent(context) {\n * // Traverse into nested regions using the context API\n * const regions = context.visitRegions(context.node.regions);\n *\n * if (context.node.typeId === 'commerce_assets.heroBanner') {\n * return { ...context.node, regions, data: { ...context.node.data, imageUrl: '/images/winter-sale.jpg' } };\n * }\n * return { ...context.node, regions };\n * },\n * });\n * ```\n */\nexport function transformComponent(\n component: ShopperExperience.schemas['Component'],\n visitor: PageVisitor\n): ShopperExperience.schemas['Component'] | null {\n return new RootVisitorContext(visitor).visitComponent(component);\n}\n\n/**\n * Applies the visitor to a single region. If the visitor's `visitRegion`\n * handler is defined, it receives a {@link VisitorContext} and is responsible\n * for traversing into the region's child components using `context.visitComponents()`.\n * If no `visitRegion` handler is defined, child components are traversed\n * automatically. Returns `null` to exclude the region and all its children\n * from the result.\n *\n * @param region - The region to transform.\n * @param visitor - The visitor with callbacks.\n * @returns The transformed region, or `null` to exclude it.\n *\n * @example\n * ```ts\n * import { transformRegion } from '@salesforce/storefront-next-runtime/design/data';\n *\n * // Filter empty regions and traverse into non-empty ones\n * const emptyRegion = { id: 'sidebar', components: [] };\n * const populatedRegion = { id: 'main', components: [\n * { id: 'product-grid', typeId: 'commerce_assets.productGrid', regions: [] },\n * ]};\n *\n * const visitor = {\n * visitRegion(context) {\n * if (!context.node.components?.length) {\n * return null; // Remove empty regions\n * }\n * // Traverse into child components using the context API\n * const components = context.visitComponents(context.node.components);\n * return { ...context.node, components };\n * },\n * };\n *\n * transformRegion(emptyRegion, visitor); // => null (removed)\n * transformRegion(populatedRegion, visitor); // => { id: 'main', components: [...] }\n * ```\n */\nexport function transformRegion(\n region: ShopperExperience.schemas['Region'],\n visitor: PageVisitor\n): ShopperExperience.schemas['Region'] | null {\n return new RootVisitorContext(visitor).visitRegion(region);\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { ComponentDataBinding, DataBindingRequirement, QualifierContext, ResolvedDataBinding } from '../types';\nimport type { ShopperExperience } from '@/scapi-client/types';\n\n/**\n * Pattern matching bare expressions: `type.field`.\n */\nconst BARE_EXPRESSION_PATTERN = /^(\\w+)\\.(\\w+)$/;\n\n/**\n * Coerces a string value returned by the data binding API into a boolean or\n * number when the contents represent one. The data provider returns every\n * field as a string, so callers expecting typed values would otherwise receive\n * `\"true\"` instead of `true` or `\"2026\"` instead of `2026`.\n *\n * Non-string inputs are returned as-is. Strings that are neither booleans nor\n * finite numbers are returned unchanged.\n */\nexport function parseFieldValue(value: unknown): unknown {\n if (typeof value !== 'string') return value;\n if (value === 'true') return true;\n if (value === 'false') return false;\n if (value.trim() === '') return value;\n const num = Number(value);\n if (Number.isFinite(num)) return num;\n return value;\n}\n\n/**\n * Parses a binding expression string into its provider type and field name.\n * Supports the bare `type.field` format.\n *\n * @param expression - The expression string to parse.\n * @returns The parsed type and field, or `null` if the expression is invalid.\n *\n * @example\n * ```ts\n * parseExpression('content_asset.title'); // { type: 'content_asset', field: 'title' }\n * parseExpression('invalid'); // null\n * ```\n */\nexport function parseExpression(expression: string): { type: string; field: string } | null {\n const match = expression.trim().match(BARE_EXPRESSION_PATTERN);\n if (match) {\n return { type: match[1], field: match[2] };\n }\n\n return null;\n}\n\n/**\n * Resolves a single binding expression against the component's data contexts\n * and the resolved data bindings from context resolution.\n *\n * Returns the resolved field value, or an empty string if the expression is\n * invalid, the matching context or record is not found, or the field does not\n * exist on the resolved record.\n *\n * @param expression - The expression string (e.g. `\"content_asset.body\"`).\n * @param contexts - The component's data binding contexts.\n * @param dataBindings - The resolved data bindings from {@link QualifierContext}.\n * @returns The resolved value, or `''` if resolution fails.\n */\nexport function resolveExpression(\n expression: string,\n contexts: DataBindingRequirement[],\n dataBindings: NonNullable<QualifierContext['dataBindings']>\n): unknown {\n const parsed = parseExpression(expression);\n if (!parsed) return '';\n\n const context = contexts.find((c) => c.type === parsed.type);\n if (!context) return '';\n\n const record: ResolvedDataBinding | undefined = dataBindings[context.type]?.[context.id];\n if (!record) return '';\n\n return parseFieldValue(record[parsed.field] ?? '');\n}\n\n/**\n * Resolves data binding expressions for a single component. Replaces attribute\n * values in the component's `data` with the resolved values from context\n * resolution. Attributes without a matching expression are preserved as-is.\n * When an expression cannot be resolved, the attribute value is set to an\n * empty string.\n *\n * Returns the component unchanged if it has no data binding metadata or if\n * `dataBindings` is `undefined`.\n *\n * @param component - The component to resolve data bindings for.\n * @param binding - The component's data binding metadata from the page manifest's `componentInfo`, or `null`/`undefined` if not bound.\n * @param dataBindings - The resolved data bindings from {@link QualifierContext}, or `undefined` if no bindings were resolved.\n * @returns The component with resolved attribute values, or the original component if no bindings apply.\n *\n * @example\n * ```ts\n * import { resolveComponentDataBindings } from '@salesforce/storefront-next-runtime/design/data';\n *\n * const component = {\n * id: 'banner',\n * typeId: 'commerce_assets.contentBanner',\n * data: { heading: 'Fallback Title', body: 'Fallback Body' },\n * regions: [],\n * };\n *\n * const binding = {\n * expressions: {\n * heading: 'content_asset.title',\n * body: 'content_asset.body',\n * },\n * contexts: [{ type: 'content_asset', id: 'winter-sale-uuid' }],\n * };\n *\n * const dataBindings = {\n * content_asset: {\n * 'winter-sale-uuid': {\n * title: 'Winter Sale',\n * body: '<div>Free Shipping on all orders!</div>',\n * },\n * },\n * };\n *\n * const resolved = resolveComponentDataBindings(component, binding, dataBindings);\n * // resolved.data.heading === 'Winter Sale'\n * // resolved.data.body === '<div>Free Shipping on all orders!</div>'\n * ```\n */\nexport function resolveComponentDataBindings(\n component: ShopperExperience.schemas['Component'],\n binding: ComponentDataBinding | null | undefined,\n dataBindings: QualifierContext['dataBindings']\n): ShopperExperience.schemas['Component'] {\n if (!dataBindings) {\n return component;\n }\n\n if (!binding?.contexts?.length) return component;\n\n const expressionEntries = Object.entries(binding.expressions ?? {});\n if (expressionEntries.length === 0) return component;\n\n const resolvedData: Record<string, unknown> = {\n ...(component.data as Record<string, unknown> | undefined),\n };\n\n for (const [attrName, expression] of expressionEntries) {\n resolvedData[attrName] = resolveExpression(expression, binding.contexts, dataBindings);\n }\n\n return {\n ...component,\n data: resolvedData as typeof component.data,\n };\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/**\n * Rewrites `?$staticlink$` placeholders in Page Designer markup attributes\n * into fully-qualified static-content URLs using the caller-supplied\n * {@link AttributeResolutionContext}.\n *\n * Pipeline-action placeholders (`$link-...$`, `$url(...)$`, `$httpUrl(...)$`,\n * `$httpsUrl(...)$`, `$include(...)$`) are intentionally NOT rewritten.\n * Storefront-next components use React composition for navigation rather than\n * ECOM pipeline routing, so these placeholders pass through as-is.\n */\nimport type { AttributeResolutionContext } from './attribute-resolution';\n\nconst STATICLINK_PATTERN = /\\?\\$staticlink\\$/gi;\n\nconst STATICLINK_DELIMITERS_SINGLE = '\":=\\'(>';\nconst STATICLINK_DELIMITERS_DOUBLE = ['\"[', '=[', ',[', ' [', ' ,', ', '];\n\nlet warnedStaticlink = false;\n\nfunction rewriteImages(source: string, ctx: AttributeResolutionContext): string {\n const domain = ctx.pageLibraryDomain;\n\n if (!domain) {\n // Fire once per process via the module-level dedup flag, then\n // route the structured warning to the consumer's onWarn handler.\n // typeId / attrId / attrType are intentionally empty — the\n // pageLibraryDomain miss is a manifest-level configuration issue\n // and not attributable to a specific component attribute.\n if (!warnedStaticlink) {\n warnedStaticlink = true;\n ctx.onWarn?.({\n kind: 'staticlink-rewrite-skipped',\n message: '?$staticlink$ rewrite skipped: ctx.pageLibraryDomain is not set',\n typeId: '',\n attrId: '',\n attrType: 'markup',\n });\n }\n return source;\n }\n\n const resolveStaticUrl = ctx.staticLinkFor ?? ctx.resolveMediaUrl;\n\n let result = '';\n let lastPos = -1;\n\n STATICLINK_PATTERN.lastIndex = 0;\n let match = STATICLINK_PATTERN.exec(source);\n\n if (!match) {\n return source;\n }\n\n while (match) {\n const pos = match.index;\n const newPos = STATICLINK_PATTERN.lastIndex;\n\n // Walk backwards to find the start of the filename\n let startPos = pos - 1;\n\n while (true) {\n if (startPos <= lastPos) {\n break;\n }\n\n const ch = source.charAt(startPos);\n\n if (STATICLINK_DELIMITERS_SINGLE.indexOf(ch) !== -1) {\n // ECOM exception: '=' followed by '.' is not a delimiter (CMS images with encoded paths)\n if (!(ch === '=' && startPos + 1 < source.length && source.charAt(startPos + 1) === '.')) {\n break;\n }\n }\n\n if (startPos > 0) {\n const doubleChar = source.substring(startPos - 1, startPos + 1);\n if (STATICLINK_DELIMITERS_DOUBLE.includes(doubleChar)) {\n break;\n }\n }\n\n startPos--;\n }\n\n // Append left part (between last match end and filename start)\n const leftStart = lastPos === -1 ? 0 : lastPos;\n result += source.substring(leftStart, startPos + 1);\n\n // Extract path\n const path = source.substring(startPos + 1, pos);\n\n if (path.trim().length !== 0) {\n let url = resolveStaticUrl({\n libraryDomain: domain,\n path: path.trim(),\n locale: ctx.locale,\n });\n\n if (path.startsWith(' ')) {\n url = ` ${url}`;\n }\n if (path.endsWith(' ')) {\n url += ' ';\n }\n\n result += url;\n }\n\n lastPos = newPos;\n match = STATICLINK_PATTERN.exec(source);\n }\n\n // Append remainder\n const tailStart = lastPos === -1 ? 0 : lastPos;\n result += source.substring(tailStart);\n\n return result;\n}\n\n/**\n * Rewrites `?$staticlink$` placeholders in markup to fully-qualified\n * static-content URLs. Pipeline-action placeholders pass through unchanged.\n */\nexport function rewriteMarkup(source: string, ctx: AttributeResolutionContext): string {\n if (!source) {\n return '';\n }\n\n return rewriteImages(source, ctx);\n}\n\n/** @internal Test-only: resets the staticlink warning flag. */\nexport function _resetStaticLinkWarningForTesting(): void {\n warnedStaticlink = false;\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n/**\n * Per-attribute resolution for Page Designer manifests. Walks a component's\n * already-locale-merged `data` map and converts each attribute's manifest\n * envelope into the wire shape the SCAPI `getPage` controller would have\n * returned for the same component.\n *\n * Module is platform-neutral: imports nothing from `template-retail-rsc-app`,\n * `site-context/build-url`, or React Router. The caller (storefront-next\n * middleware or Page Designer preview) supplies an\n * {@link AttributeResolutionContext} that injects URL-building utilities, so\n * the same code runs in both consumers.\n *\n * The dispatch table covers `image`, `markup`/`url`, `file`, and `cms_record`.\n *\n * When the `componentTypes` map is unavailable, image dispatch falls back to\n * **structural** detection (presence of `media.libraryDomain` and\n * `media.path`). When `componentTypes` is wired through, dispatch keys off\n * {@link AttributeDefinition.type} and unknown types pass through with a\n * one-time warning (Q9 forward-compat).\n */\nimport { rewriteMarkup } from './markup-url-rewriter';\n\n/**\n * Per-request resolution surface. Storefront-next builds one of these from\n * the request URL + site config; Page Designer preview builds one against\n * the BM origin. Both surfaces inject URL-building utilities so this module\n * stays platform-neutral.\n */\nexport interface AttributeResolutionContext {\n /**\n * Storefront origin used to absolutize URLs, e.g.\n * `\"https://www.shop.example\"`. Page Designer preview supplies the BM\n * origin instead.\n */\n host: string;\n\n /**\n * Builds a static-content URL for a media-file path inside a library.\n * Mirrors ECOM's `MediaFile.getAbsURL()` chain, parameterized by the\n * storefront request rather than a JVM `Request`.\n *\n * The {@code locale} hint is optional — when omitted, the resolver\n * substitutes `\"default\"` so URLs still resolve.\n */\n resolveMediaUrl: (ref: { libraryDomain: string; path: string; locale?: string }) => string;\n\n /**\n * Resolves a library-relative path inside markup (`?$staticlink$`).\n * When omitted, falls back to {@link resolveMediaUrl}.\n */\n staticLinkFor?: (ref: { libraryDomain: string; path: string; locale?: string }) => string;\n\n /**\n * Default library media domain used when rewriting `?$staticlink$`\n * references inside markup attributes. Sourced from\n * {@code manifest.pageLibraryDomain} and threaded through by the\n * caller. Optional — when omitted, `?$staticlink$` placeholders inside\n * markup are left untouched and a one-time warning fires.\n */\n pageLibraryDomain?: string;\n\n /**\n * Locale hint forwarded to {@link resolveMediaUrl}. Page Designer\n * preview may omit this when the editor session has no locale; the\n * resolver substitutes `\"default\"` in that case.\n */\n locale?: string;\n\n /**\n * Optional handler invoked when the resolver encounters a recoverable\n * problem — malformed envelopes, unknown attribute types, depth limits\n * exceeded. Lets the consumer route these into its own logger / metric\n * pipeline instead of the SDK calling `console.warn` directly.\n *\n * The runtime dedupes calls to {@code onWarn} per\n * `(typeId, attrId, attrType)` triple so a misshapen value processed\n * many times only fires the handler once per process.\n *\n * When omitted the runtime stays silent — fits unit tests and Page\n * Designer preview where stderr noise is undesirable. Production\n * callers should supply a handler that forwards to their structured\n * logger.\n */\n onWarn?: (warning: AttributeResolutionWarning) => void;\n}\n\n/**\n * Payload passed to {@link AttributeResolutionContext.onWarn}. Keep this\n * shape stable — consumers may pattern-match on `kind` to decide log\n * level, attach extra metadata, etc.\n */\nexport interface AttributeResolutionWarning {\n /**\n * Identifier for the kind of issue, useful for routing or grouping in\n * downstream logging:\n *\n * - `malformed-image` / `malformed-file` / `malformed-cms-record` —\n * the manifest envelope didn't match the expected shape and the\n * value is being passed through unchanged.\n * - `unknown-attribute-type` — the runtime saw an attribute type it\n * doesn't recognize (forward-compat from a newer ECOM).\n * - `cms-record-depth-exceeded` — recursive cms_record nesting hit\n * the resolver's safety limit.\n * - `staticlink-rewrite-skipped` — markup contains `?$staticlink$`\n * placeholders but `ctx.pageLibraryDomain` was not configured, so\n * the placeholder is left in the source. Fires once per process\n * regardless of how many markup attributes hit it (tracked via the\n * {@code typeId}/{@code attrId} fields, both empty strings, in the\n * {@code warnOnce} dedup key).\n */\n kind:\n | 'malformed-image'\n | 'malformed-file'\n | 'malformed-cms-record'\n | 'cms-record-depth-exceeded'\n | 'unknown-attribute-type'\n | 'staticlink-rewrite-skipped';\n /** Human-readable message — safe to log directly. */\n message: string;\n /** Component type id the offending attribute belongs to. */\n typeId: string;\n /** Attribute id within the component. */\n attrId: string;\n /** The attribute's declared type, when known. Empty for inner cms_record entries that don't carry a type. */\n attrType: string;\n}\n\n/**\n * Slim attribute definition used by the resolver to dispatch by type.\n * Mirrors the fields {@code AttributeDefinition} ships in SCAPI's\n * `componentTypes` map. Defined here so the resolver doesn't take a\n * dependency on the larger SCAPI generated types.\n */\nexport interface AttributeDefinition {\n /** Attribute identifier as authored by the merchant (e.g. `\"hero\"`). */\n id: string;\n /**\n * Lower-case attribute type identifier matching ECOM's\n * {@code AttributeDefinition.Type#getID}. Examples:\n * `\"string\"`, `\"text\"`, `\"image\"`, `\"markup\"`, `\"file\"`, `\"cms_record\"`.\n */\n type: string;\n /**\n * Default value declared on the attribute definition. Used by component\n * data composition as a fallback when neither the active locale nor the\n * fallback locale has a value for this attribute (see\n * {@code processPage}'s `visitComponent`). The shape is whatever the\n * attribute's `type` would normally hold — a string for `string`/`text`,\n * an envelope for `image`/`file`, etc.\n */\n defaultValue?: unknown;\n}\n\n/**\n * Image envelope wire shape emitted by ECOM at manifest build time. Built\n * by {@code ManifestService.serializeImageAttribute}. The {@code media}\n * sub-object carries the library domain segment and the host-agnostic path;\n * MRT stamps the URL using {@link AttributeResolutionContext.resolveMediaUrl}.\n */\ninterface ImageEnvelope {\n focalPoint?: { x: number; y: number };\n metaData?: { width: number; height: number };\n media: { libraryDomain: string; path: string };\n}\n\n/**\n * Wire shape MRT emits after host-stamping. Matches the SCAPI `ImageWO_v1`\n * shape: focal point, metadata, and a fully-qualified URL — no `media`\n * sub-object, no top-level `path`.\n */\ninterface ResolvedImage {\n focalPoint?: { x: number; y: number };\n metaData?: { width: number; height: number };\n url: string;\n}\n\n/**\n * Module-scoped dedup set for unknown-type / malformed-envelope warnings.\n * Keyed by `${kind}|${typeId}|${attrId}|${attrType}` so two different\n * issues on the same attribute (e.g. malformed-image then later\n * unknown-type) both fire once.\n */\nconst warnedKeys = new Set<string>();\n\n/**\n * Routes a structured warning to the consumer's `onWarn` handler at most\n * once per `(kind, typeId, attrId, attrType)` triple. When no handler is\n * configured the runtime stays silent — production callers are expected to\n * supply a handler.\n */\nfunction warnOnce(\n ctx: AttributeResolutionContext,\n kind: AttributeResolutionWarning['kind'],\n typeId: string,\n attrId: string,\n attrType: string,\n message: string\n): void {\n if (!ctx.onWarn) return;\n\n const key = `${kind}|${typeId}|${attrId}|${attrType}`;\n if (warnedKeys.has(key)) return;\n warnedKeys.add(key);\n\n ctx.onWarn({ kind, message, typeId, attrId, attrType });\n}\n\n/**\n * Test-only: clears the dedup set so repeated runs of the same key inside a\n * test process can each emit a warning. Production callers should never\n * import this — it's intentionally unexported from the package barrel.\n *\n * @internal\n */\nexport function _resetWarnedKeysForTesting(): void {\n warnedKeys.clear();\n}\n\n/**\n * Returns true when `value` is shaped like an {@link ImageEnvelope}. Used\n * during structural dispatch (when `componentTypes` is unavailable) to\n * recognize image attributes without `attrDef.type`.\n */\nfunction isImageEnvelope(value: unknown): value is ImageEnvelope {\n if (!value || typeof value !== 'object') {\n return false;\n }\n\n const candidate = value as Record<string, unknown>;\n const media = candidate.media as Record<string, unknown> | undefined;\n\n return (\n media != null &&\n typeof media === 'object' &&\n typeof media.libraryDomain === 'string' &&\n typeof media.path === 'string'\n );\n}\n\n/**\n * Converts an {@link ImageEnvelope} to the resolved SCAPI shape by stamping\n * the URL. Returns the original value untouched if the envelope is\n * malformed (missing `media.libraryDomain` or `media.path`); a warning is\n * logged once per `(typeId, attrId, attrType)` triple so production logs\n * don't drown.\n */\nfunction resolveImageAttribute(\n value: unknown,\n typeId: string,\n attrId: string,\n attrType: string,\n ctx: AttributeResolutionContext\n): ResolvedImage | unknown {\n if (!isImageEnvelope(value)) {\n warnOnce(\n ctx,\n 'malformed-image',\n typeId,\n attrId,\n attrType,\n 'malformed image envelope, passing through unchanged'\n );\n\n return value;\n }\n\n const url = ctx.resolveMediaUrl({\n libraryDomain: value.media.libraryDomain,\n path: value.media.path,\n locale: ctx.locale,\n });\n\n const out: ResolvedImage = { url };\n\n if (value.focalPoint) {\n out.focalPoint = value.focalPoint;\n }\n\n if (value.metaData) {\n out.metaData = value.metaData;\n }\n\n return out;\n}\n\n/**\n * File envelope wire shape emitted by ECOM at manifest build time.\n * Contains only the `media` sub-object with library domain and path.\n */\ninterface FileEnvelope {\n media: { libraryDomain: string; path: string };\n}\n\nfunction isFileEnvelope(value: unknown): value is FileEnvelope {\n if (!value || typeof value !== 'object') {\n return false;\n }\n\n const candidate = value as Record<string, unknown>;\n const media = candidate.media as Record<string, unknown> | undefined;\n\n return (\n media != null &&\n typeof media === 'object' &&\n typeof media.libraryDomain === 'string' &&\n typeof media.path === 'string' &&\n !('focalPoint' in candidate || 'metaData' in candidate)\n );\n}\n\n/**\n * Resolves a file envelope to a URL string. Matches SCAPI's\n * `mediaFile.getAbsURL().toString()` — file attributes emit a plain URL\n * string, not an object envelope.\n */\nfunction resolveFileAttribute(\n value: unknown,\n typeId: string,\n attrId: string,\n ctx: AttributeResolutionContext\n): string | unknown {\n if (!isFileEnvelope(value)) {\n warnOnce(ctx, 'malformed-file', typeId, attrId, 'file', 'malformed file envelope, passing through unchanged');\n return value;\n }\n\n return ctx.resolveMediaUrl({\n libraryDomain: value.media.libraryDomain,\n path: value.media.path,\n locale: ctx.locale,\n });\n}\n\n/**\n * CMS Record envelope wire shape. Contains the record's own type\n * (with attribute definitions) and the nested attribute map.\n */\ninterface CmsRecordEnvelope {\n id: string;\n type: { id: string; name?: string; attributeDefinitions: AttributeDefinition[] };\n attributes: Record<string, unknown>;\n}\n\nconst MAX_CMS_RECORD_DEPTH = 10;\n\nfunction isCmsRecordEnvelope(value: unknown): value is CmsRecordEnvelope {\n if (!value || typeof value !== 'object') {\n return false;\n }\n\n const candidate = value as Record<string, unknown>;\n\n if (typeof candidate.id !== 'string') {\n return false;\n }\n\n const type = candidate.type as Record<string, unknown> | undefined;\n\n if (!type || typeof type !== 'object' || typeof type.id !== 'string') {\n return false;\n }\n\n if (!Array.isArray(type.attributeDefinitions)) {\n return false;\n }\n\n return candidate.attributes != null && typeof candidate.attributes === 'object';\n}\n\nfunction resolveCmsRecordAttribute(\n value: unknown,\n typeId: string,\n attrId: string,\n ctx: AttributeResolutionContext,\n depth: number\n): unknown {\n if (value == null) {\n return value;\n }\n\n if (!isCmsRecordEnvelope(value)) {\n warnOnce(\n ctx,\n 'malformed-cms-record',\n typeId,\n attrId,\n 'cms_record',\n 'malformed cms_record envelope, passing through unchanged'\n );\n return value;\n }\n\n if (depth >= MAX_CMS_RECORD_DEPTH) {\n warnOnce(\n ctx,\n 'cms-record-depth-exceeded',\n typeId,\n attrId,\n 'cms_record',\n `cms_record nesting depth exceeded (max ${MAX_CMS_RECORD_DEPTH}), passing through unchanged`\n );\n return value;\n }\n\n const innerDefs = value.type.attributeDefinitions;\n const resolvedAttrs = resolveCmsRecordInnerAttributes(value.attributes, typeId, innerDefs, ctx, depth + 1);\n\n return {\n id: value.id,\n type: value.type,\n attributes: resolvedAttrs,\n };\n}\n\nfunction resolveCmsRecordInnerAttributes(\n data: Record<string, unknown>,\n typeId: string,\n defs: AttributeDefinition[],\n ctx: AttributeResolutionContext,\n depth: number\n): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n const defsById = new Map<string, AttributeDefinition>();\n\n for (const def of defs) {\n defsById.set(def.id, def);\n }\n\n for (const [attrId, value] of Object.entries(data)) {\n const def = defsById.get(attrId);\n\n if (!def) {\n out[attrId] = value;\n continue;\n }\n\n out[attrId] = dispatchCmsRecordInner(value, typeId, attrId, def, ctx, depth);\n }\n\n return out;\n}\n\nfunction dispatchCmsRecordInner(\n value: unknown,\n typeId: string,\n attrId: string,\n attrDef: AttributeDefinition,\n ctx: AttributeResolutionContext,\n depth: number\n): unknown {\n if (attrDef.type === 'cms_record') {\n return resolveCmsRecordAttribute(value, typeId, attrId, ctx, depth);\n }\n\n return dispatchByType(value, typeId, attrId, attrDef, ctx);\n}\n\n/**\n * Resolves every attribute on a component's `data` map to the wire shape\n * SCAPI `getPage` would have returned.\n *\n * Dispatch is type-driven when {@code typeAttributeDefinitions} is supplied.\n * Otherwise the resolver inspects each value structurally — it recognizes\n * the image envelope by the presence of `media.libraryDomain` and\n * `media.path` and passes everything else through unchanged.\n *\n * Forward-compatibility (Q9): unknown attribute types pass through. Each\n * `(typeId, attrId, attrType)` triple is logged once per process via a\n * module-scoped dedup set.\n *\n * @param data attribute map to resolve, already\n * locale-merged + data-binding-resolved by\n * {@link processPage}.\n * @param typeId component type identifier, used as part\n * of the dedup key for warnings. Empty\n * string is acceptable for anonymous\n * callers (page-level data).\n * @param typeAttributeDefinitions attribute definitions for {@code typeId}\n * from `manifest.componentTypes`. When\n * omitted, falls back to structural\n * detection of the image envelope.\n * @param ctx per-request resolution surface.\n * @returns a new map with each attribute's value replaced by the resolved\n * wire shape; pass-through for any attribute type the resolver\n * doesn't yet recognize.\n */\nexport function resolveAttributeValues(\n data: Record<string, unknown> | undefined | null,\n typeId: string,\n typeAttributeDefinitions: Record<string, AttributeDefinition> | undefined,\n ctx: AttributeResolutionContext\n): Record<string, unknown> {\n if (!data) {\n return {};\n }\n\n const out: Record<string, unknown> = {};\n\n if (typeAttributeDefinitions && Object.keys(typeAttributeDefinitions).length > 0) {\n for (const [attrId, value] of Object.entries(data)) {\n const def = typeAttributeDefinitions[attrId];\n\n if (!def) {\n out[attrId] = value;\n continue;\n }\n\n out[attrId] = dispatchByType(value, typeId, attrId, def, ctx);\n }\n\n return out;\n }\n\n // No type definitions to dispatch on. Use structural detection for the\n // one attribute we know how to recognize (image envelope) and pass\n // everything else through.\n for (const [attrId, value] of Object.entries(data)) {\n if (isImageEnvelope(value)) {\n out[attrId] = resolveImageAttribute(value, typeId, attrId, 'image', ctx);\n } else {\n out[attrId] = value;\n }\n }\n\n return out;\n}\n\n/**\n * Type-driven dispatch. Unknown types fall through with a deduped warning\n * (Q9) — the principle is that a runtime older than ECOM should still\n * produce *something* rather than dropping the value.\n */\nfunction dispatchByType(\n value: unknown,\n typeId: string,\n attrId: string,\n attrDef: AttributeDefinition,\n ctx: AttributeResolutionContext\n): unknown {\n switch (attrDef.type) {\n case 'image':\n return resolveImageAttribute(value, typeId, attrId, attrDef.type, ctx);\n\n case 'markup':\n return typeof value === 'string' ? rewriteMarkup(value, ctx) : value;\n\n case 'file':\n return resolveFileAttribute(value, typeId, attrId, ctx);\n\n case 'cms_record':\n return resolveCmsRecordAttribute(value, typeId, attrId, ctx, 0);\n\n case 'string':\n case 'text':\n case 'url':\n case 'boolean':\n case 'integer':\n case 'enum':\n case 'custom':\n case 'product':\n case 'category':\n case 'page':\n return value;\n\n default:\n warnOnce(\n ctx,\n 'unknown-attribute-type',\n typeId,\n attrId,\n attrDef.type,\n 'unknown attribute type, passing through unchanged'\n );\n\n return value;\n }\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { QualifierContext, VisibilityRuleDef } from './types';\n\n/**\n * Evaluates a visibility rule against a shopper's qualifier context.\n *\n * Campaign-based and non-campaign rules are **mutually exclusive** paths,\n * matching the server's `VisibilityDefinition.isVisible()` logic:\n *\n * - **Campaign-based rule** (has `campaignQualifiers`): only the campaign\n * qualifiers are checked. Schedule, locale, and customer-group fields are\n * ignored because the campaign qualification already incorporates those\n * checks server-side.\n * - **Non-campaign rule**: locale, schedule, AND customer groups are checked.\n * All specified conditions must pass.\n *\n * When no context is provided and the rule requires campaign or customer group\n * checks, those checks will fail (returning `false`). Schedule checks do not\n * require context and are evaluated against `Date.now()`.\n *\n * @param rule - The visibility rule to evaluate.\n * @param locale - The current locale (e.g. `\"en_US\"`). Used to check whether the rule applies to this locale.\n * @param context - The shopper's active qualifiers, or `null`/`undefined` if not yet resolved.\n * @returns `true` if the rule's conditions pass, `false` otherwise.\n *\n * @example\n * ```ts\n * import { validateRule } from '@salesforce/storefront-next-runtime/design/data';\n *\n * // Campaign-based rule — only campaign qualifiers are evaluated\n * const campaignRule = {\n * activeLocales: ['en_US'],\n * campaignQualifiers: [{ campaignId: 'holiday-sale-2026', promotionId: 'free-shipping' }],\n * };\n *\n * // Non-campaign rule — locale, schedule AND customer groups are evaluated\n * const segmentRule = {\n * activeLocales: ['en_US', 'fr_FR'],\n * customerGroups: ['vip-customers'],\n * schedule: {\n * start: new Date('2026-12-01').toISOString(),\n * end: new Date('2026-12-31').toISOString(),\n * },\n * };\n * ```\n */\nexport function validateRule(rule: VisibilityRuleDef, locale: string, context?: QualifierContext | null): boolean {\n // Campaign-based rules and non-campaign rules are mutually exclusive\n // paths, mirroring the server's if/else-if branching.\n if (rule.campaignQualifiers?.length) {\n for (const campaignQualifier of rule.campaignQualifiers) {\n if (!context?.campaignQualifiers?.[campaignQualifier.campaignId]?.[campaignQualifier.promotionId]) {\n return false;\n }\n }\n } else {\n if (rule.activeLocales && !rule.activeLocales.includes(locale)) {\n return false;\n }\n\n // Rule schedule times are in ISO 8601 format, so we need to convert them to milliseconds\n if (rule.schedule) {\n const now = Date.now();\n\n if (rule.schedule.start) {\n const startTimeInMillis = new Date(rule.schedule.start).getTime();\n\n // If the start time is invalid, the rule fails\n if (Number.isNaN(startTimeInMillis) || startTimeInMillis >= now) {\n return false;\n }\n }\n\n if (rule.schedule.end) {\n const endTimeInMillis = new Date(rule.schedule.end).getTime();\n\n // If the end time is invalid, the rule fails\n if (Number.isNaN(endTimeInMillis) || endTimeInMillis <= now) {\n return false;\n }\n }\n }\n\n if (rule.customerGroups) {\n for (const customerGroup of rule.customerGroups) {\n if (!context?.customerGroups?.[customerGroup]) {\n return false;\n }\n }\n }\n }\n\n return true;\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { transformPage } from './transform';\nimport { resolveComponentDataBindings } from './resolve-data-bindings';\nimport {\n resolveAttributeValues,\n type AttributeDefinition,\n type AttributeResolutionContext,\n} from './attribute-resolution';\nimport { validateRule } from '../validate-rule';\nimport type { QualifierContext, PageManifest, VariationEntry, RegionInfo } from '../types';\nimport type { ShopperExperience } from '@/scapi-client/types';\n\n/**\n * Context required for page processing. Contains the shopper's runtime\n * qualifiers, the component-level visibility rules, and the locale used\n * to resolve locale-specific component content from the page manifest.\n */\nexport interface PageProcessorContext {\n /** The shopper's active qualifiers (campaigns, customer groups), or `null` if not resolved. */\n qualifiers: QualifierContext | null;\n /** Component visibility rule definitions extracted from the page layout. */\n componentInfo: PageManifest['componentInfo'];\n /** Page-level region configuration (e.g. maxComponents limits) for top-level regions not nested under a component. */\n pageInfo: {\n regions: VariationEntry['regions'];\n };\n /** The locale to use when resolving locale-specific component content (e.g. `\"en_US\"`). */\n locale: string;\n /** The site's default locale, used as a fallback when the current locale has no content entry (e.g. `\"en_US\"`). */\n defaultLocale: string;\n /**\n * Per-request resolution surface used by {@link resolveAttributeValues} to\n * convert manifest envelopes into the wire shape SCAPI `getPage` would have\n * returned. The storefront-next middleware builds it once per request and\n * Page Designer preview supplies an editor-mode equivalent.\n */\n attrCtx: AttributeResolutionContext;\n /**\n * Per-component-type attribute definitions hoisted by the manifest builder.\n * Keyed by `typeId`. Optional — when omitted, the resolver falls back to\n * structural detection for the image envelope and passes everything else\n * through.\n */\n componentTypes?: Record<string, { attributeDefinitions: Record<string, AttributeDefinition> }>;\n /**\n * When `true` (default), invisible components are removed from the tree and\n * regions are truncated to their `maxComponents` limit. When `false`, invisible\n * components and overflow components are kept in the tree but marked with\n * `visible: false` — used in design/preview mode so the editor can display them.\n */\n pruneInvisible?: boolean;\n}\n\n/**\n * Filters a page's components based on their visibility rules and resolves\n * data binding expressions in a single traversal. Traverses the page tree\n * using the visitor pattern and:\n *\n * 1. Removes any component whose visibility rules do not pass against the\n * shopper's qualifier context.\n * 2. Resolves data binding expressions in each surviving component's `data`\n * attributes using the resolved data bindings from context resolution.\n *\n * A component is visible if **any** of its visibility rules pass (OR logic).\n * If a component has rules and none of them pass, it is removed. Components\n * without rules are always included.\n *\n * @param page - The page to process.\n * @param context - The processing context with qualifier data, visibility rules, and resolved data bindings.\n * @returns A new page with invisible components filtered out and data binding expressions resolved.\n *\n * @example\n * ```ts\n * import { processPage } from '@salesforce/storefront-next-runtime/design/data';\n *\n * const page = {\n * id: 'homepage',\n * typeId: 'storePage',\n * regions: [{\n * id: 'main',\n * components: [\n * { id: 'public-banner', typeId: 'commerce_assets.heroBanner', regions: [] },\n * { id: 'loyalty-offer', typeId: 'commerce_assets.promoTile', regions: [] },\n * ],\n * }],\n * };\n *\n * // The \"loyalty-offer\" component requires the shopper to be in \"loyalty-members\"\n * const componentInfo = {\n * 'public-banner': { visibilityRules: [] },\n * 'loyalty-offer': {\n * visibilityRules: [{ customerGroups: ['loyalty-members'] }],\n * },\n * };\n *\n * // Guest shopper — not in any customer group\n * const filtered = processPage(page, {\n * qualifiers: { customerGroups: {}, campaignQualifiers: {} },\n * componentInfo,\n * });\n * // filtered.regions[0].components has only \"public-banner\"\n * // \"loyalty-offer\" was removed because the shopper isn't a loyalty member\n * ```\n */\n/**\n * Builds a component's `data` map by walking each attribute definition and\n * picking the first non-undefined value in priority order:\n *\n * active-locale content → fallback-locale content → attrDef.defaultValue\n *\n * If none of those have a value the attribute is omitted from the result.\n *\n * When no `typeDefs` are supplied, we fall back to the legacy behavior:\n * `{ ...nodeData, ...defaultContent, ...localeContent }`. This keeps\n * already-deployed manifests rendering until the manifest builder starts\n * emitting `componentTypes`.\n */\nfunction composeComponentData({\n nodeData,\n defaultContent,\n localeContent,\n typeDefs,\n}: {\n nodeData: Record<string, unknown> | undefined;\n defaultContent: Record<string, unknown>;\n localeContent: Record<string, unknown>;\n typeDefs: Record<string, AttributeDefinition> | undefined;\n}): Record<string, unknown> {\n if (!typeDefs || Object.keys(typeDefs).length === 0) {\n return {\n ...(nodeData ?? {}),\n ...defaultContent,\n ...localeContent,\n };\n }\n\n const result: Record<string, unknown> = {};\n\n for (const attrId of Object.keys(typeDefs)) {\n const def = typeDefs[attrId];\n\n if (Object.prototype.hasOwnProperty.call(localeContent, attrId)) {\n result[attrId] = localeContent[attrId];\n } else if (Object.prototype.hasOwnProperty.call(defaultContent, attrId)) {\n result[attrId] = defaultContent[attrId];\n } else if (def.defaultValue !== undefined) {\n result[attrId] = def.defaultValue;\n }\n }\n\n return result;\n}\n\nexport function processPage(\n page: ShopperExperience.schemas['Page'],\n processorContext: PageProcessorContext\n): ShopperExperience.schemas['Page'] {\n const { pruneInvisible = true } = processorContext;\n\n return transformPage(page, {\n visitPage(ctx) {\n // Page-level `data` is rare today (most pages carry no top-level\n // attributes), but the schema permits it and SCAPI passes whatever\n // is there straight through. Run the resolver so any image-typed\n // page attribute lights up the same way component attributes do.\n // We only emit a `data` property when the source page had one, to\n // match the SCAPI shape (which omits the field for pages without\n // top-level attributes).\n const pageNode = ctx.node;\n const result: ShopperExperience.schemas['Page'] = {\n ...pageNode,\n regions: ctx.visitRegions(pageNode.regions),\n };\n\n if (pageNode.data !== undefined) {\n const typeDefs = processorContext.componentTypes?.[pageNode.typeId]?.attributeDefinitions;\n result.data = resolveAttributeValues(\n pageNode.data as Record<string, unknown>,\n pageNode.typeId,\n typeDefs,\n processorContext.attrCtx\n ) as typeof pageNode.data;\n }\n\n return result;\n },\n visitRegion(ctx) {\n let regionInfo: RegionInfo | undefined;\n\n if (ctx.parent?.type === 'page') {\n regionInfo = processorContext.pageInfo.regions[ctx.node.id];\n } else if (ctx.parent?.type === 'component') {\n regionInfo = processorContext.componentInfo[ctx.parent.node.id]?.regions?.[ctx.node.id];\n }\n\n // Visit each component first — this runs visitComponent which\n // filters out components that fail their visibility rules.\n let components = ctx.visitComponents(ctx.node.components);\n\n if (regionInfo?.maxComponents != null) {\n if (pruneInvisible) {\n components = components.slice(0, regionInfo.maxComponents);\n } else {\n const result: ShopperExperience.schemas['Component'][] = [];\n let visibleCount = 0;\n\n for (const comp of components) {\n if (comp.visible) {\n visibleCount++;\n }\n\n if (visibleCount > regionInfo.maxComponents) {\n result.push({ ...comp, visible: false });\n } else {\n result.push(comp);\n }\n }\n\n components = result;\n }\n }\n\n return {\n ...ctx.node,\n components,\n };\n },\n visitComponent(ctx) {\n const componentInfo = processorContext.componentInfo[ctx.node.id];\n const visibilityRules = componentInfo?.visibilityRules ?? [];\n let isVisible = true;\n\n // Visibility rules use OR logic: the component is visible\n // if ANY rule passes. Only remove it when it has its own\n // rules and none of them pass.\n if (visibilityRules.length > 0) {\n const anyRulePassed = visibilityRules.some((rule) =>\n validateRule(rule, processorContext.locale, processorContext.qualifiers)\n );\n\n if (!anyRulePassed) {\n if (pruneInvisible) {\n return null;\n }\n\n isVisible = false;\n }\n }\n\n // Compose the component's `data` map per attribute definition with\n // resolution priority: active-locale content → fallback-locale\n // content → attribute-definition default value → key omitted.\n // When no type definitions are available, fall back to the legacy\n // merge so existing manifests still resolve.\n const defaultContent = componentInfo?.content?.[processorContext.defaultLocale] ?? {};\n const localeContent = componentInfo?.content?.[processorContext.locale] ?? {};\n const isLocalized = Boolean(componentInfo?.content?.[processorContext.locale]);\n const typeDefs = processorContext.componentTypes?.[ctx.node.typeId]?.attributeDefinitions;\n\n const composedData = composeComponentData({\n nodeData: ctx.node.data as Record<string, unknown> | undefined,\n defaultContent,\n localeContent,\n typeDefs,\n });\n\n const name = componentInfo?.name ?? ctx.node.name;\n const fragment = componentInfo?.fragment ?? ctx.node.fragment ?? false;\n\n let node: ShopperExperience.schemas['Component'] = {\n ...ctx.node,\n name,\n fragment,\n localized: isLocalized,\n visible: isVisible,\n data: composedData as typeof ctx.node.data,\n };\n\n // Resolve data binding expressions (overrides content for bound attributes).\n node = resolveComponentDataBindings(\n node,\n componentInfo?.dataBinding,\n processorContext.qualifiers?.dataBindings\n );\n\n // Stamp attribute envelopes with the per-request URL/host/route info.\n // Runs *after* the data-binding overlay so any binding-resolved values\n // are also passed through the resolver (e.g. markup/url rewriting).\n const resolvedData = resolveAttributeValues(\n node.data as Record<string, unknown> | undefined,\n node.typeId,\n typeDefs,\n processorContext.attrCtx\n );\n\n node = {\n ...node,\n data: resolvedData as typeof node.data,\n };\n\n return {\n ...node,\n regions: ctx.visitRegions(ctx.node.regions),\n };\n },\n }) as ShopperExperience.schemas['Page'];\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nexport class RequiredError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'RequiredError';\n }\n\n static assert<TValue>(\n value: TValue,\n message: string,\n isEmpty: (value: TValue) => boolean = (v) => v == null\n ): asserts value is NonNullable<TValue> {\n if (isEmpty(value)) {\n throw new RequiredError(message);\n }\n }\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { SiteManifest } from '../types';\n\n/**\n * The result of resolving an identifier through a content assignment resolver.\n * Contains the object type, aspect type, and ordered list of keys to search\n * in the site manifest's content assignments.\n */\nexport interface ResolvedContentAssignmentLookup {\n /** The type of commerce object (e.g. `'product'`, `'category'`). */\n objectType: string;\n /** Ordered list of object IDs to search in the site manifest's content assignments. */\n keys: string[];\n}\n\n/**\n * A function that converts an identifier key (e.g., a product or category ID)\n * into a {@link ResolvedContentAssignmentLookup} describing where to search\n * in the site manifest for the assigned page ID.\n */\nexport type ContentAssignmentResolver = (\n key: string,\n manifest?: SiteManifest | null\n) => ResolvedContentAssignmentLookup;\n\n/**\n * Registry of content assignment resolvers keyed by {@link IdentifierType}.\n * Each resolver knows how to convert its identifier type into a set of lookup\n * keys for the site manifest.\n *\n * Built-in resolvers:\n * - **`'product'`** — Maps a product ID to a single PDP lookup key.\n * - **`'category'`** — Maps a category ID to an ordered list of keys that\n * traverses the category hierarchy from child to root, enabling inherited\n * page assignments.\n *\n * The `'page'` identifier type has no resolver — page IDs are used directly.\n *\n * @example\n * ```ts\n * import { ContentAssignmentResolvers } from '@salesforce/storefront-next-runtime/design/data';\n *\n * // Resolve a product identifier for PDP lookup\n * const productResolver = ContentAssignmentResolvers.get('product');\n * productResolver('nike-air-max-90');\n * // => { objectType: 'product', aspectType: 'pdp', keys: ['nike-air-max-90'] }\n *\n * // Resolve a category identifier — traverses hierarchy to find inherited assignments\n * const categoryResolver = ContentAssignmentResolvers.get('category');\n * const siteManifest = {\n * categories: {\n * 'mens-running-shoes': { name: 'Running Shoes', parentCategory: 'mens-shoes' },\n * 'mens-shoes': { name: \"Men's Shoes\", parentCategory: 'mens' },\n * 'mens': { name: 'Men' },\n * },\n * contentObjectAssignments: {},\n * };\n * categoryResolver('mens-running-shoes', siteManifest);\n * // => { objectType: 'category', aspectType: 'plp', keys: ['mens-running-shoes', 'mens-shoes', 'mens'] }\n * ```\n */\nexport const ContentAssignmentResolvers = new Map<string, ContentAssignmentResolver>([\n [\n 'product',\n (key) => ({\n objectType: 'product',\n keys: [key],\n }),\n ],\n [\n 'category',\n (key, manifest) => {\n const keys = [];\n const visited = new Set<string>();\n\n let currentCategoryId: string | undefined = key;\n\n while (currentCategoryId && !visited.has(currentCategoryId)) {\n visited.add(currentCategoryId);\n keys.push(currentCategoryId);\n currentCategoryId = manifest?.categories[currentCategoryId]?.parentCategory;\n }\n\n return {\n objectType: 'category',\n keys,\n };\n },\n ],\n]);\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { SiteManifest, IdentifierType } from '../types';\nimport { ContentAssignmentResolvers } from './content-assignment-resolvers';\n\n/**\n * Converts a product or category identifier into a page ID by looking up\n * content assignments in the site manifest. For categories, the lookup\n * traverses the category hierarchy from the given category up to the root,\n * returning the first matching assignment.\n *\n * Returns `null` if no content assignment is found for the identifier or if\n * the identifier type has no registered resolver.\n *\n * @param options - The resolution options.\n * @param options.id - The identifier to resolve (product ID, category ID, or page ID).\n * @param options.identifierType - The type of identifier: `'product'`, `'category'`, or `'page'`.\n * @param options.siteManifest - The site manifest containing content assignments and category hierarchy.\n * @returns The resolved page ID, or `null` if no assignment was found.\n *\n * @example\n * ```ts\n * import { resolveDynamicPageId } from '@salesforce/storefront-next-runtime/design/data';\n *\n * const siteManifest = {\n * contentObjectAssignments: {\n * plp: {\n * category: {\n * 'mens-shoes': {\n * lookupMode: 'category-explicit',\n * contentId: 'page-mens-shoes-plp',\n * },\n * },\n * },\n * },\n * categories: {\n * 'mens-running-shoes': { name: 'Running Shoes', parentCategory: 'mens-shoes' },\n * 'mens-shoes': { name: \"Men's Shoes\" },\n * },\n * };\n *\n * // Direct match\n * resolveDynamicPageId({ id: 'mens-shoes', identifierType: 'category', siteManifest });\n * // => 'page-mens-shoes-plp'\n *\n * // Inherited from parent category\n * resolveDynamicPageId({ id: 'mens-running-shoes', identifierType: 'category', siteManifest });\n * // => 'page-mens-shoes-plp' (found via parent traversal)\n *\n * // No assignment found\n * resolveDynamicPageId({ id: 'womens-shoes', identifierType: 'category', siteManifest });\n * // => null\n * ```\n */\nexport function resolveDynamicPageId<TIdentifier extends IdentifierType = IdentifierType>({\n id,\n identifierType,\n siteManifest,\n aspectType,\n}: {\n id: string;\n identifierType: TIdentifier;\n aspectType: string;\n siteManifest?: SiteManifest | null;\n}): string | null {\n const resolvedContentAssignmentLookup = ContentAssignmentResolvers.get(identifierType)?.(id, siteManifest);\n\n if (resolvedContentAssignmentLookup) {\n for (const key of resolvedContentAssignmentLookup.keys) {\n const contentAssignment =\n siteManifest?.contentObjectAssignments?.[aspectType]?.[resolvedContentAssignmentLookup.objectType]?.[\n key\n ];\n\n if (contentAssignment) {\n return contentAssignment.contentId;\n }\n }\n }\n\n return null;\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { ContextResolver, PageManifest, QualifierContext, VariationEntry } from '../types';\nimport { validateRule } from '../validate-rule';\n\n/**\n * Selects the appropriate page variation from a manifest by evaluating each\n * variation's visibility rule in order. Returns the first variation whose rule\n * passes, or falls back to the manifest's default variation.\n *\n * The qualifier context is resolved lazily — the `contextResolver` is only\n * called when a variation's `ruleRequiresContext` flag is `true`, and only\n * once (the result is cached for subsequent variations).\n *\n * @param manifest - The page manifest containing all variations.\n * @param options - Resolution options.\n * @param options.contextResolver - Optional async function that returns the shopper's qualifier context. Only called if a variation's rule needs it.\n * @param options.locale - The current locale (e.g. `\"en_US\"`). Used to evaluate locale-based visibility rules.\n * @returns The selected variation entry and resolved context, or `null` if no variation (including default) exists.\n *\n * @example\n * ```ts\n * import { getPageFromManifest } from '@salesforce/storefront-next-runtime/design/data';\n *\n * const manifest = {\n * pageId: 'homepage',\n * context: { campaignQualifiers: [], customerGroups: ['vip-customers'], dataBindings: [] },\n * variationOrder: ['vip-homepage', 'holiday-homepage'],\n * variations: {\n * 'vip-homepage': {\n * ruleRequiresContext: true,\n * pageRequiresContext: false,\n * visibilityRule: { activeLocales: ['en-US'], customerGroups: ['vip-customers'] },\n * page: { id: 'homepage', typeId: 'storePage', regions: [] },\n * regions: {},\n * },\n * 'holiday-homepage': {\n * ruleRequiresContext: false,\n * pageRequiresContext: false,\n * visibilityRule: {\n * activeLocales: ['en-US'],\n * schedule: {\n * start: new Date('2026-12-01').toISOString(),\n * end: new Date('2026-12-31').toISOString(),\n * },\n * },\n * page: { id: 'homepage', typeId: 'storePage', regions: [] },\n * regions: {},\n * },\n * 'default-homepage': {\n * ruleRequiresContext: false,\n * pageRequiresContext: false,\n * page: { id: 'homepage', typeId: 'storePage', regions: [] },\n * regions: {},\n * },\n * },\n * defaultVariation: 'default-homepage',\n * componentInfo: {},\n * };\n *\n * // VIP shopper — matches first variation\n * const result = await getPageFromManifest(manifest, {\n * locale: 'en-US',\n * contextResolver: async () => ({\n * customerGroups: { 'vip-customers': true },\n * campaignQualifiers: {},\n * }),\n * });\n * // result.entry === manifest.variations['vip-homepage']\n *\n * // Non-VIP shopper outside holiday window — falls back to default\n * const fallback = await getPageFromManifest(manifest, {\n * locale: 'en-US',\n * contextResolver: async () => ({\n * customerGroups: {},\n * campaignQualifiers: {},\n * }),\n * });\n * // fallback.entry === manifest.variations['default-homepage']\n * ```\n */\nexport async function getPageFromManifest(\n manifest: PageManifest,\n {\n contextResolver,\n locale,\n }: {\n contextResolver?: ContextResolver;\n locale: string;\n }\n): Promise<{\n entry: VariationEntry;\n context: QualifierContext | null;\n} | null> {\n let context: QualifierContext | null = null;\n let resolvedVariation: VariationEntry | null = null;\n\n for (const variationId of manifest.variationOrder) {\n const variation = manifest.variations[variationId];\n\n if (variation?.ruleRequiresContext && !context) {\n context = (await contextResolver?.(manifest.context)) ?? null;\n }\n\n if (!variation?.visibilityRule || validateRule(variation.visibilityRule, locale, context)) {\n resolvedVariation = variation;\n break;\n }\n }\n\n if (!resolvedVariation) {\n resolvedVariation = manifest.variations[manifest.defaultVariation];\n }\n\n if (!resolvedVariation) {\n return null;\n }\n\n return {\n entry: resolvedVariation,\n context,\n };\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type {\n IdentifierType,\n ManifestStorage,\n ContextResolver,\n QualifierContext,\n PageMetadataOverlay,\n VariationEntry,\n} from '../types';\nimport type { ShopperExperience } from '@/scapi-client/types';\nimport { ContentAssignmentResolvers } from '../manifest/content-assignment-resolvers';\nimport { resolveDynamicPageId } from '../manifest/resolve-dynamic-page-id';\nimport { getPageFromManifest } from '../manifest/get-page';\nimport { processPage } from './process-page';\nimport type { AttributeResolutionContext } from './attribute-resolution';\nimport { RequiredError } from '../errors/required';\n\n/**\n * Page metadata fields the manifest builder may locale-overlay. Used by\n * {@link applyPageMetadataOverlay} to know which keys to copy from the\n * overlay onto the resolved page; structural fields like `id`, `typeId`,\n * and `regions` are intentionally excluded.\n */\nconst PAGE_METADATA_OVERLAY_KEYS = [\n 'name',\n 'aspectTypeId',\n 'description',\n 'pageTitle',\n 'pageDescription',\n 'pageKeywords',\n] as const satisfies readonly (keyof PageMetadataOverlay)[];\n\n/**\n * Applies a per-locale page metadata overlay to the variation's default-locale\n * page. The overlay is a **full replacement** for the listed metadata fields\n * — when a key is present in the overlay it wins; when absent we fall through\n * to the default-locale value (Q6 of the design plan).\n *\n * Returns a shallow copy of the page with overlaid fields applied. Structural\n * fields (`id`, `typeId`, `regions`, `data`) are never touched.\n */\nfunction applyPageMetadataOverlay(variation: VariationEntry, locale: string): ShopperExperience.schemas['Page'] {\n const overlay = variation.pageContent?.[locale];\n\n if (!overlay) {\n return variation.page;\n }\n\n const out: ShopperExperience.schemas['Page'] = { ...variation.page };\n\n for (const key of PAGE_METADATA_OVERLAY_KEYS) {\n if (overlay[key] !== undefined) {\n out[key] = overlay[key];\n }\n }\n\n return out;\n}\n\n/**\n * Main entry point for the page resolution pipeline. Orchestrates the full flow:\n *\n * 1. **Resolve dynamic page ID** — For product/category identifiers, looks up\n * the assigned page ID via content assignments in the site manifest.\n * 2. **Fetch page manifest** — Loads all variations for the resolved page.\n * 3. **Select variation** — Evaluates visibility rules to pick the right variation.\n * 4. **Load qualifier context** — Lazily fetches the shopper's context only if needed.\n * 5. **Process page** — Filters out components that fail visibility rules.\n *\n * Returns `null` if the page ID cannot be resolved, the manifest doesn't exist,\n * or no variation is available.\n *\n * @param options - The resolution options.\n * @param options.id - The identifier to resolve (product ID, category ID, or page ID).\n * @param options.identifierType - The type of identifier: `'product'`, `'category'`, or `'page'`.\n * @param options.locale - The locale to resolve the page for (e.g. `\"en-US\"`).\n * @param options.manifestStorage - Storage implementation for fetching manifests.\n * @param options.contextResolver - Optional async function that returns the shopper's qualifier context. Only called if a visibility rule needs it.\n * @param options.aspectType - The aspect type to resolve the page for when the identifier type is `'product'` or `'category'`.\n * @param options.pruneInvisible - When `true` (default), invisible and overflow components are removed. When `false`, they are kept but marked `visible: false` for design/preview mode.\n * @returns The fully resolved and filtered page, or `null`.\n *\n * @example\n * ```ts\n * import { resolvePage } from '@salesforce/storefront-next-runtime/design/data';\n *\n * // Resolve the PDP page for a specific product with an active holiday campaign\n * const page = await resolvePage({\n * id: 'nike-air-max-90',\n * identifierType: 'product',\n * aspectType: 'pdp',\n * locale: 'en-US',\n * manifestStorage: {\n * async getPageManifest(id) {\n * // Fetch from CDN, filesystem, or database\n * return fetchManifest(`/manifests/${id}.json`);\n * },\n * async getSiteManifest() {\n * return fetchManifest('/manifests/site.json');\n * },\n * },\n * contextResolver: async () => ({\n * customerGroups: { 'vip-customers': true },\n * campaignQualifiers: {\n * 'holiday-sale-2026': { 'free-shipping': true },\n * },\n * }),\n * });\n *\n * if (page) {\n * // page.regions contains only components visible to this VIP shopper\n * // during the holiday sale campaign\n * renderPage(page);\n * }\n * ```\n */\nexport async function resolvePage({\n id,\n identifierType,\n aspectType,\n locale,\n defaultLocale,\n manifestStorage,\n contextResolver,\n attrCtx,\n pruneInvisible = true,\n}: {\n id: string;\n identifierType: IdentifierType;\n aspectType?: string;\n locale: string;\n defaultLocale: string;\n manifestStorage: ManifestStorage;\n contextResolver?: ContextResolver;\n /**\n * Per-request resolution surface for attribute envelope rewriting. Built\n * once per request by the storefront-next middleware (or Page Designer\n * preview). The `componentTypes` map travels on the\n * {@link PageManifest} itself and is read off the manifest below before\n * being threaded into {@link processPage}.\n */\n attrCtx: AttributeResolutionContext;\n pruneInvisible?: boolean;\n}): Promise<ShopperExperience.schemas['Page'] | null> {\n let resolvedId: string | null = null;\n\n if (ContentAssignmentResolvers.has(identifierType)) {\n const siteManifest = await manifestStorage.getSiteManifest();\n\n RequiredError.assert(aspectType, `Aspect type is required for identifier type ${identifierType}`, (v) => !v);\n\n resolvedId = resolveDynamicPageId({ id, identifierType, aspectType, siteManifest });\n } else {\n resolvedId = id;\n }\n\n if (!resolvedId) {\n return null;\n }\n\n const pageManifest = await manifestStorage.getPageManifest(resolvedId);\n\n if (!pageManifest) {\n return null;\n }\n\n const pageResults = await getPageFromManifest(pageManifest, {\n contextResolver,\n locale,\n });\n\n if (!pageResults) {\n return null;\n }\n\n let context: QualifierContext | null = null;\n\n if (pageResults.entry.pageRequiresContext) {\n context = pageResults.context ?? (await contextResolver?.(pageManifest.context)) ?? null;\n }\n\n // Apply per-locale page metadata overlay before processing. The overlay\n // carries the SCAPI-shape page metadata fields (`name`, `aspectTypeId`,\n // `description`, `pageTitle`, `pageDescription`, `pageKeywords`) that may\n // differ per locale. When the request locale isn't in `pageContent`, we\n // fall back to the default-locale page on `variation.page`. Q6 of the\n // design plan locks in full-replacement semantics; see\n // {@link applyPageMetadataOverlay} for the field-by-field policy.\n const localizedPage = applyPageMetadataOverlay(pageResults.entry, locale);\n\n // Thread manifest-level pageLibraryDomain onto the resolution context so\n // the markup rewriter can resolve ?$staticlink$ without the caller having\n // to know the library domain up-front (B.2 — the manifest is the source\n // of truth for this value).\n const resolvedAttrCtx =\n pageManifest.pageLibraryDomain && !attrCtx.pageLibraryDomain\n ? { ...attrCtx, pageLibraryDomain: pageManifest.pageLibraryDomain }\n : attrCtx;\n\n return processPage(localizedPage, {\n qualifiers: context,\n componentInfo: pageManifest.componentInfo,\n pageInfo: {\n regions: pageResults.entry.regions,\n },\n locale,\n defaultLocale,\n attrCtx: resolvedAttrCtx,\n // `componentTypes` lives on the manifest. May be `undefined` for\n // older manifests; the optional typing on `PageProcessorContext`\n // covers that case.\n componentTypes: pageManifest.componentTypes,\n pruneInvisible,\n });\n}\n"],"mappings":"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"}
|
package/dist/design-mode.d.ts
CHANGED
|
@@ -17,12 +17,13 @@
|
|
|
17
17
|
/**
|
|
18
18
|
* Utility functions for detecting active design/preview modes
|
|
19
19
|
*/
|
|
20
|
+
type PageDesignerMode = 'EDIT' | 'PREVIEW';
|
|
20
21
|
/**
|
|
21
22
|
* Get the mode parameter from URL search params
|
|
22
23
|
* @param url - Optional URL string or Request object for server-side usage. If not provided, uses window.location on client-side
|
|
23
24
|
* @returns The mode parameter value or null if not found
|
|
24
25
|
*/
|
|
25
|
-
declare const getUrlMode: (url?: string | URL | Request) =>
|
|
26
|
+
declare const getUrlMode: (url?: string | URL | Request) => PageDesignerMode | null;
|
|
26
27
|
/**
|
|
27
28
|
* Check if design mode is active
|
|
28
29
|
* @param url - Optional URL string or Request object for server-side usage
|
|
@@ -36,5 +37,5 @@ declare const isDesignModeActive: (url?: string | URL | Request) => boolean;
|
|
|
36
37
|
*/
|
|
37
38
|
declare const isPreviewModeActive: (url?: string | URL | Request) => boolean;
|
|
38
39
|
//#endregion
|
|
39
|
-
export { getUrlMode, isDesignModeActive, isPreviewModeActive };
|
|
40
|
+
export { PageDesignerMode, getUrlMode, isDesignModeActive, isPreviewModeActive };
|
|
40
41
|
//# sourceMappingURL=design-mode.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"design-mode.d.ts","names":[],"sources":["../src/design/modeDetection.ts"],"sourcesContent":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"design-mode.d.ts","names":[],"sources":["../src/design/modeDetection.ts"],"sourcesContent":[],"mappings":";;AAoBA;AAOA;;;;;AA0BA;AAOA;;;;;;;;;;KAxCY,gBAAA;;;;;;cAOC,4BAA6B,MAAM,YAAU;;;;;;cA0B7C,oCAAqC,MAAM;;;;;;cAO3C,qCAAsC,MAAM"}
|
package/dist/events.d.ts
CHANGED
|
@@ -114,10 +114,36 @@ interface ClickSearchSuggestionEvent extends BaseEvent {
|
|
|
114
114
|
searchInputText: string;
|
|
115
115
|
suggestion: string;
|
|
116
116
|
}
|
|
117
|
-
/**
|
|
118
|
-
interface
|
|
119
|
-
eventType: '
|
|
120
|
-
surface: '
|
|
117
|
+
/** Wishlist item added by shopper */
|
|
118
|
+
interface WishlistItemAddedEvent extends BaseEvent {
|
|
119
|
+
eventType: 'wishlist_item_added';
|
|
120
|
+
surface: 'pdp' | 'plp' | 'cart' | 'wishlist-page';
|
|
121
|
+
productId: string;
|
|
122
|
+
}
|
|
123
|
+
/** Wishlist item removed by shopper */
|
|
124
|
+
interface WishlistItemRemovedEvent extends BaseEvent {
|
|
125
|
+
eventType: 'wishlist_item_removed';
|
|
126
|
+
surface: 'pdp' | 'plp' | 'cart' | 'wishlist-page';
|
|
127
|
+
productId: string;
|
|
128
|
+
}
|
|
129
|
+
/** Wishlist page viewed */
|
|
130
|
+
interface WishlistViewedEvent extends BaseEvent {
|
|
131
|
+
eventType: 'wishlist_viewed';
|
|
132
|
+
}
|
|
133
|
+
/** Individual product merged from guest to registered wishlist */
|
|
134
|
+
interface WishlistItemMergedEvent extends BaseEvent {
|
|
135
|
+
eventType: 'wishlist_item_merged';
|
|
136
|
+
productId: string;
|
|
137
|
+
}
|
|
138
|
+
/** Summary of guest wishlist merge operation on login */
|
|
139
|
+
interface WishlistMergedEvent extends BaseEvent {
|
|
140
|
+
eventType: 'wishlist_merged';
|
|
141
|
+
merged: number;
|
|
142
|
+
skipped: number;
|
|
143
|
+
failed: number;
|
|
144
|
+
mergedProductIds: string[];
|
|
145
|
+
skippedProductIds: string[];
|
|
146
|
+
failedProductIds: string[];
|
|
121
147
|
}
|
|
122
148
|
/**
|
|
123
149
|
* Interface for custom analytics events.
|
|
@@ -138,7 +164,7 @@ interface AnalyticsEventExtensions {}
|
|
|
138
164
|
*
|
|
139
165
|
* Custom types can be added by extending the AnalyticsEventExtensions interface.
|
|
140
166
|
*/
|
|
141
|
-
type AnalyticsEvent = ViewPageEvent | ViewProductEvent | ViewSearchEvent | ViewCategoryEvent | ViewRecommenderEvent | ClickProductInCategoryEvent | ClickProductInSearchEvent | ClickProductInRecommenderEvent | CartItemAddEvent | CheckoutStartEvent | CheckoutStepEvent | ViewSearchSuggestionEvent | ClickSearchSuggestionEvent |
|
|
167
|
+
type AnalyticsEvent = ViewPageEvent | ViewProductEvent | ViewSearchEvent | ViewCategoryEvent | ViewRecommenderEvent | ClickProductInCategoryEvent | ClickProductInSearchEvent | ClickProductInRecommenderEvent | CartItemAddEvent | CheckoutStartEvent | CheckoutStepEvent | ViewSearchSuggestionEvent | ClickSearchSuggestionEvent | WishlistItemAddedEvent | WishlistItemRemovedEvent | WishlistViewedEvent | WishlistItemMergedEvent | WishlistMergedEvent | AnalyticsEventExtensions[keyof AnalyticsEventExtensions];
|
|
142
168
|
/**
|
|
143
169
|
* Helper type for mapping event_type to the corresponding event type.
|
|
144
170
|
*/
|
|
@@ -237,5 +263,5 @@ declare function getEventMediator(getAdapters: () => EventAdapter[]): EventMedia
|
|
|
237
263
|
*/
|
|
238
264
|
declare function resetEventMediator(): void;
|
|
239
265
|
//#endregion
|
|
240
|
-
export { AnalyticsEvent, AnalyticsEventExtensions, AnalyticsPayload, AnalyticsUser, BaseEvent, CartItemAddEvent, CheckoutStartEvent, CheckoutStepEvent, ClickProductInCategoryEvent, ClickProductInRecommenderEvent, ClickProductInSearchEvent, ClickSearchSuggestionEvent,
|
|
266
|
+
export { AnalyticsEvent, AnalyticsEventExtensions, AnalyticsPayload, AnalyticsUser, BaseEvent, CartItemAddEvent, CheckoutStartEvent, CheckoutStepEvent, ClickProductInCategoryEvent, ClickProductInRecommenderEvent, ClickProductInSearchEvent, ClickSearchSuggestionEvent, ConsentCategory, ConsentPreferences, EventAdapter, EventMediator, EventPayload, EventSiteInfo, EventTypeMap, PayloadTbd, ViewCategoryEvent, ViewPageEvent, ViewProductEvent, ViewRecommenderEvent, ViewSearchEvent, ViewSearchSuggestionEvent, WishlistItemAddedEvent, WishlistItemMergedEvent, WishlistItemRemovedEvent, WishlistMergedEvent, WishlistViewedEvent, createEvent, getEventMediator, resetEventMediator, sendViewPageEvent };
|
|
241
267
|
//# sourceMappingURL=events.d.ts.map
|
package/dist/events.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.d.ts","names":[],"sources":["../src/events/types.ts","../src/events/events.ts","../src/events/mediator.ts"],"sourcesContent":[],"mappings":";;;;;KAmBK,iBAAA,GAAoB,gBAAA,CAAiB,OAqEQ,CAAA,aAAA,CAAA,GArEiB,gBAAA,CAAiB,OAqElC,CAAA,aAAA,CAAA;AAQlD,KA5EK,MAAA,GAAS,gBAAA,CAAiB,OA4EI,CAAA,QAAA,CAAA,GA5EgB,gBAAA,CAAiB,OA4EjC,CAAA,QAAA,CAAA;;;;;;AAQnC;AAOA;;;;;AAMA;AAMA;AAOA;;AAEe,UA3FE,aAAA,CA2FF;EAF2B,QAAA,EAAA,YAAA,GAAA,OAAA;EAAS,IAAA,CAAA,EAAA,MAAA;EAKlC,GAAA,CAAA,EAAA,MAAA;EAKA,SAAA,CAAA,EAAA,MAAA;EAOA,UAAA,CAAA,EAAA,MAAA;EAMA,UAAA,CAAA,EAAA,MAAA;EAOA,SAAA,CAAA,EAAA,MAAA;
|
|
1
|
+
{"version":3,"file":"events.d.ts","names":[],"sources":["../src/events/types.ts","../src/events/events.ts","../src/events/mediator.ts"],"sourcesContent":[],"mappings":";;;;;KAmBK,iBAAA,GAAoB,gBAAA,CAAiB,OAqEQ,CAAA,aAAA,CAAA,GArEiB,gBAAA,CAAiB,OAqElC,CAAA,aAAA,CAAA;AAQlD,KA5EK,MAAA,GAAS,gBAAA,CAAiB,OA4EI,CAAA,QAAA,CAAA,GA5EgB,gBAAA,CAAiB,OA4EjC,CAAA,QAAA,CAAA;;;;;;AAQnC;AAOA;;;;;AAMA;AAMA;AAOA;;AAEe,UA3FE,aAAA,CA2FF;EAF2B,QAAA,EAAA,YAAA,GAAA,OAAA;EAAS,IAAA,CAAA,EAAA,MAAA;EAKlC,GAAA,CAAA,EAAA,MAAA;EAKA,SAAA,CAAA,EAAA,MAAA;EAOA,UAAA,CAAA,EAAA,MAAA;EAMA,UAAA,CAAA,EAAA,MAAA;EAOA,SAAA,CAAA,EAAA,MAAA;EAOA,QAAA,CAAA,EAAA,MAAA;EAOA,KAAA,CAAA,EAAA,MAAA;AAKjB;AAMA;AAwBA;AASA;;AAEM,UAlKW,UAAA,CAkKX;;;;;AAMA,KAhKM,gBAAA,GAAmB,aAgKzB,GAhKyC,UAgKzC;AACA,KA3JM,SAAA,GA2JN;EACA,SAAA,EAAA,MAAA;EACA,OAAA,EA3JO,gBA2JP;EACA,UAAA,CAAA,EAAA,MAAA;CACA;AACA,UA1JW,aAAA,SAAsB,SA0JjC,CAAA;EACA,SAAA,EAAA,WAAA;EACA,IAAA,EAAA,MAAA;;AAEA,UAzJW,gBAAA,SAAyB,SAyJpC,CAAA;EACA,SAAA,EAAA,cAAA;EAA+B,OAAA,EAxJxB,eAAA,CAAgB,OAwJQ,CAAA,SAAA,CAAA;;AAKzB,UA1JK,eAAA,SAAwB,SA0JjB,CAAA;EACd,SAAA,EAAA,aAAA;EAAkB,eAAA,EAAA,MAAA;EAAiB,aAAA,EAxJ1B,aAAA,CAAc,OAwJY,CAAA,kBAAA,CAAA,EAAA;EAAC,IAAA,EAAA,MAAA;EAMlC,WAAA,EA5JK,aAAA,CAAc,OA4JP,CAAA,qBAAA,CAAA,CAAA,qBAAA,CAAA;;AAA+C,UAzJtD,iBAAA,SAA0B,SAyJ4B,CAAA;EAAa,SAAA,EAAA,eAAA;EAAlB,QAAA,EAvJpD,eAAA,CAAgB,OAuJoC,CAAA,UAAA,CAAA;EACrD,aAAA,EAvJM,aAAA,CAAc,OAuJpB,CAAA,kBAAA,CAAA,EAAA;EAAgB,IAAA,EAAA,MAAA;EAQjB,WAAA,EA7JK,aAAA,CAAc,OA6JN,CAAA,qBAAA,CAAA,CAAA,qBAAA,CAAA;AAoBzB;AAQY,UAtLK,oBAAA,SAA6B,SAsLE,CAAA;EAM/B,SAAA,EAAA,kBAAY;EAGd,aAAA,EAAA,MAAA;EACI,eAAA,EAAA,MAAA;EACU,QAAA,EA7Lf,aAAA,CAAc,OA6LC,CAAA,kBAAA,CAAA,EAAA;;AACb,UA3LC,2BAAA,SAAoC,SA2LrC,CAAA;EAOJ,SAAA,EAAA,2BAAa;EACN,QAAA,EAjML,eAAA,CAAgB,OAiMX,CAAA,UAAA,CAAA;EAA2B,OAAA,EAhMjC,aAAA,CAAc,OAgMmB,CAAA,kBAAA,CAAA;;AAAsD,UA7LnF,yBAAA,SAAkC,SA6LiD,CAAA;;;WA1LvF,aAAA,CAAc;AChF3B;AAAsC,UDmFrB,8BAAA,SAAuC,SCnFlB,CAAA;EACvB,SAAA,EAAA,8BAAA;EACQ,aAAA,EAAA,MAAA;EAAb,eAAA,EAAA,MAAA;EACP,OAAA,EDoFU,aAAA,CAAc,OCpFxB,CAAA,kBAAA,CAAA;;AAAc,UDuFA,gBAAA,SAAyB,SCvFzB,CAAA;EAgBD,SAAA,EAAA,eAAiB;EACtB,SAAA,EDwEI,KCxEJ,CDwEU,iBCxEV,CAAA;;AAEI,UDyEE,kBAAA,SAA2B,SCzE7B,CAAA;EACU,SAAA,EAAA,gBAAA;EAAkB,MAAA,ED0E/B,MC1E+B;;UD6E1B,iBAAA,SAA0B;;EEvF3B,QAAA,EAAA,MAAA;EAqBA,UAAA,EAAA,MAAA;UFsEJ;;UAGK,yBAAA,SAAkC;;;eAGlC;;UAGA,0BAAA,SAAmC;;;;;;UAOnC,sBAAA,SAA+B;;;;;;UAO/B,wBAAA,SAAiC;;;;;;UAOjC,mBAAA,SAA4B;;;;UAK5B,uBAAA,SAAgC;;;;;UAMhC,mBAAA,SAA4B;;;;;;;;;;;;;;;;;;;;;;UAwB5B,wBAAA;;;;;;KASL,cAAA,GACN,gBACA,mBACA,kBACA,oBACA,uBACA,8BACA,4BACA,iCACA,mBACA,qBACA,oBACA,4BACA,6BACA,yBACA,2BACA,sBACA,0BACA,sBACA,+BAA+B;;;;KAKzB,YAAA,WACF,kBAAkB,iBAAiB;;;;KAMjC,uBAAuB,+BAA+B,KAAK,aAAa;WACvE;;;KAQD,aAAA;;;;;;;;;;;;;;;;;;;KAoBA,eAAA;;;;;;;KAQA,kBAAA,GAAqB;;;;;UAMhB,YAAA;;sBAGF,2BACI,oCACU,uBACpB;;;;;;KAOG,aAAA;iBACO,2BAA2B,oCAAoC;;;;;AAlNlF;;;;;;AAQA;AAOA;;;;;AAMA;AAMiB,iBCnFD,WDmFC,CAAA,UCnFqB,cDuFzB,CAAA,WAJ2C,CAAA,CAAA,CAAA,SAAS,EClFlD,CDkFkD,EAAA,IAAA,ECjFvD,YDiFuD,CCjF1C,CDiF0C,CAAA,CAAA,EChF9D,YDgF8D,CChFjD,CDgFiD,CAAA;AAOjE;;;;;AAKA;AAKA;AAOA;AAMA;AAOiB,iBCrGD,iBAAA,CDqGgC,KAAA,ECpGrC,aDoG8C,EAAA,aAAA,ECnGtC,aDmGsC,EAAA,QAAA,CAAA,EClG1C,aDkG0C,EAAA,kBAAA,CAAA,ECjGhC,kBDiGgC,CAAA,EAAA,IAAA;;;;AAhEzD;;;;;;AAQA;AAOiB,iBE1DD,gBAAA,CF0D6B,WAAA,EAAA,GAAA,GE1DO,YF0DP,EAAA,CAAA,EE1DwB,aF0DxB,GAAA,SAAA;;;;;AAM7C;AAMiB,iBEjDD,kBAAA,CAAA,CFiDgC,EAAA,IAInC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n-client.d.ts","names":[],"sources":["../src/i18n/types.ts","../src/i18n/client.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"i18n-client.d.ts","names":[],"sources":["../src/i18n/types.ts","../src/i18n/client.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;KAkCY,YAAA,yBAAqC;WAAmB;;;;;;;;;;;;;;;;;iBC+BpD,WAAA;;aAAsD;eAAmB;IAAiB"}
|
package/dist/i18n-client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n-client.js","names":["defaultInterpolation: InterpolationOptions"],"sources":["../src/i18n/defaults.ts","../src/i18n/client.ts"],"sourcesContent":["/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { InterpolationOptions } from 'i18next';\n\n/** Shared i18next interpolation config. Disables HTML escaping (React handles that) and adds `{{ value, number }}` formatting via `toLocaleString`. */\nexport const defaultInterpolation: InterpolationOptions = {\n escapeValue: false,\n format: (value, format) => {\n if (format === 'number' && typeof value === 'number') {\n return value.toLocaleString();\n }\n return value;\n },\n};\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport i18next, { type i18n, type BackendModule, type ReadCallback } from 'i18next';\nimport { initReactI18next } from 'react-i18next';\nimport I18nextBrowserLanguageDetector from 'i18next-browser-languagedetector';\nimport { defaultInterpolation } from './defaults.js';\nimport type { LocaleLoader } from './types.js';\n\n/**\n * Custom i18next backend that calls the provided `loadLocale` callback to dynamically\n * import translations. Keeping the import() call in the template lets Vite resolve the\n * dynamic path at build time and split translations into per-language chunks.\n */\nfunction createDynamicImportBackend(instance: i18n, loadLocale: LocaleLoader): BackendModule {\n return {\n type: 'backend',\n init() {\n // No initialization needed\n },\n read(language: string, namespace: string, callback: ReadCallback) {\n loadLocale(language)\n .then((module) => {\n const translations = module.default;\n Object.entries(translations).forEach(([ns, nsTranslations]) => {\n instance.addResourceBundle(language, ns, nsTranslations, true, true);\n });\n callback(null, translations[namespace] ?? {});\n })\n .catch((error: Error) => {\n callback(error, false);\n });\n },\n };\n}\n\n/**\n * Initialize i18next on the client side.\n * Pass a `loadLocale` callback containing the dynamic import so Vite can resolve it\n * at build time relative to the template's source tree.\n *\n * @example\n * // In root.tsx — Vite resolves the import() relative to this file\n * initI18next({\n * language: document.documentElement.lang || undefined,\n * loadLocale: (language) => import(`@/locales/${language}/index.ts`),\n * });\n */\nexport function initI18next(options?: { language?: string; instance?: i18n; loadLocale?: LocaleLoader }): i18n {\n // NOTE: For any changes to this function, verify that Vite HMR still works with translations\n\n const language = options?.language;\n const instance = options?.instance ?? i18next;\n const loadLocale = options?.loadLocale;\n\n if (language) {\n instance.language = language;\n }\n\n const i18nextInstance = instance.use(initReactI18next);\n\n if (loadLocale) {\n i18nextInstance.use(createDynamicImportBackend(instance, loadLocale));\n }\n\n if (!language) {\n i18nextInstance.use(I18nextBrowserLanguageDetector);\n }\n\n void i18nextInstance.init({\n ns: [],\n ...(language\n ? { lng: language }\n : {\n detection: { order: ['htmlTag'], caches: [] },\n }),\n interpolation: defaultInterpolation,\n });\n\n return instance;\n}\n"],"mappings":";;;;;;AAkBA,MAAaA,uBAA6C;CACtD,aAAa;CACb,SAAS,OAAO,WAAW;AACvB,MAAI,WAAW,YAAY,OAAO,UAAU,SACxC,QAAO,MAAM,gBAAgB;AAEjC,SAAO;;CAEd;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"i18n-client.js","names":["defaultInterpolation: InterpolationOptions"],"sources":["../src/i18n/defaults.ts","../src/i18n/client.ts"],"sourcesContent":["/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { InterpolationOptions } from 'i18next';\n\n/** Shared i18next interpolation config. Disables HTML escaping (React handles that) and adds `{{ value, number }}` formatting via `toLocaleString`. */\nexport const defaultInterpolation: InterpolationOptions = {\n escapeValue: false,\n format: (value, format) => {\n if (format === 'number' && typeof value === 'number') {\n return value.toLocaleString();\n }\n return value;\n },\n};\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// Browser-only entry. Imports `i18next-browser-languagedetector`, which has no Node\n// support — keep this file out of server code and only consume via the\n// `@salesforce/storefront-next-runtime/i18n/client` subpath in client modules\n// (e.g. `root.tsx` `useEffect`). Server-capable APIs live in the sibling\n// `@salesforce/storefront-next-runtime/i18n` subpath.\nimport i18next, { type i18n, type BackendModule, type ReadCallback } from 'i18next';\nimport { initReactI18next } from 'react-i18next';\nimport I18nextBrowserLanguageDetector from 'i18next-browser-languagedetector';\nimport { defaultInterpolation } from './defaults.js';\nimport type { LocaleLoader } from './types.js';\n\n/**\n * Custom i18next backend that calls the provided `loadLocale` callback to dynamically\n * import translations. Keeping the import() call in the template lets Vite resolve the\n * dynamic path at build time and split translations into per-language chunks.\n */\nfunction createDynamicImportBackend(instance: i18n, loadLocale: LocaleLoader): BackendModule {\n return {\n type: 'backend',\n init() {\n // No initialization needed\n },\n read(language: string, namespace: string, callback: ReadCallback) {\n loadLocale(language)\n .then((module) => {\n const translations = module.default;\n Object.entries(translations).forEach(([ns, nsTranslations]) => {\n instance.addResourceBundle(language, ns, nsTranslations, true, true);\n });\n callback(null, translations[namespace] ?? {});\n })\n .catch((error: Error) => {\n callback(error, false);\n });\n },\n };\n}\n\n/**\n * Initialize i18next on the client side.\n * Pass a `loadLocale` callback containing the dynamic import so Vite can resolve it\n * at build time relative to the template's source tree.\n *\n * @example\n * // In root.tsx — Vite resolves the import() relative to this file\n * initI18next({\n * language: document.documentElement.lang || undefined,\n * loadLocale: (language) => import(`@/locales/${language}/index.ts`),\n * });\n */\nexport function initI18next(options?: { language?: string; instance?: i18n; loadLocale?: LocaleLoader }): i18n {\n // NOTE: For any changes to this function, verify that Vite HMR still works with translations\n\n const language = options?.language;\n const instance = options?.instance ?? i18next;\n const loadLocale = options?.loadLocale;\n\n if (language) {\n instance.language = language;\n }\n\n const i18nextInstance = instance.use(initReactI18next);\n\n if (loadLocale) {\n i18nextInstance.use(createDynamicImportBackend(instance, loadLocale));\n }\n\n if (!language) {\n i18nextInstance.use(I18nextBrowserLanguageDetector);\n }\n\n void i18nextInstance.init({\n ns: [],\n ...(language\n ? { lng: language }\n : {\n detection: { order: ['htmlTag'], caches: [] },\n }),\n interpolation: defaultInterpolation,\n });\n\n return instance;\n}\n"],"mappings":";;;;;;AAkBA,MAAaA,uBAA6C;CACtD,aAAa;CACb,SAAS,OAAO,WAAW;AACvB,MAAI,WAAW,YAAY,OAAO,UAAU,SACxC,QAAO,MAAM,gBAAgB;AAEjC,SAAO;;CAEd;;;;;;;;;ACKD,SAAS,2BAA2B,UAAgB,YAAyC;AACzF,QAAO;EACH,MAAM;EACN,OAAO;EAGP,KAAK,UAAkB,WAAmB,UAAwB;AAC9D,cAAW,SAAS,CACf,MAAM,WAAW;IACd,MAAM,eAAe,OAAO;AAC5B,WAAO,QAAQ,aAAa,CAAC,SAAS,CAAC,IAAI,oBAAoB;AAC3D,cAAS,kBAAkB,UAAU,IAAI,gBAAgB,MAAM,KAAK;MACtE;AACF,aAAS,MAAM,aAAa,cAAc,EAAE,CAAC;KAC/C,CACD,OAAO,UAAiB;AACrB,aAAS,OAAO,MAAM;KACxB;;EAEb;;;;;;;;;;;;;;AAeL,SAAgB,YAAY,SAAmF;CAG3G,MAAM,WAAW,SAAS;CAC1B,MAAM,WAAW,SAAS,YAAY;CACtC,MAAM,aAAa,SAAS;AAE5B,KAAI,SACA,UAAS,WAAW;CAGxB,MAAM,kBAAkB,SAAS,IAAI,iBAAiB;AAEtD,KAAI,WACA,iBAAgB,IAAI,2BAA2B,UAAU,WAAW,CAAC;AAGzE,KAAI,CAAC,SACD,iBAAgB,IAAI,+BAA+B;AAGvD,CAAK,gBAAgB,KAAK;EACtB,IAAI,EAAE;EACN,GAAI,WACE,EAAE,KAAK,UAAU,GACjB,EACI,WAAW;GAAE,OAAO,CAAC,UAAU;GAAE,QAAQ,EAAE;GAAE,EAChD;EACP,eAAe;EAClB,CAAC;AAEF,QAAO"}
|
package/dist/i18n.d.ts
CHANGED
|
@@ -27,7 +27,6 @@ declare function mockI18nContext(contextProvider: RouterContextProvider, options
|
|
|
27
27
|
}): void;
|
|
28
28
|
//#endregion
|
|
29
29
|
//#region src/i18n/types.d.ts
|
|
30
|
-
|
|
31
30
|
/** Config passed to `createI18nMiddleware`. All values come from the template — the SDK never reads config values directly. */
|
|
32
31
|
interface I18nMiddlewareConfig {
|
|
33
32
|
resources: Resource;
|
|
@@ -59,5 +58,5 @@ declare function createI18nMiddleware(config: I18nMiddlewareConfig): MiddlewareF
|
|
|
59
58
|
/** Shared i18next interpolation config. Disables HTML escaping (React handles that) and adds `{{ value, number }}` formatting via `toLocaleString`. */
|
|
60
59
|
declare const defaultInterpolation: InterpolationOptions;
|
|
61
60
|
//#endregion
|
|
62
|
-
export { type I18nMiddlewareConfig, type LocaleLoader,
|
|
61
|
+
export { type I18nMiddlewareConfig, type LocaleLoader, createI18nMiddleware, defaultInterpolation, getLocale, getTranslation, mockI18nContext };
|
|
63
62
|
//# sourceMappingURL=i18n.d.ts.map
|