@salesforce/storefront-next-runtime 1.0.0-alpha.0 → 1.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ComponentContext.js +199 -4
- package/dist/ComponentContext.js.map +1 -1
- package/dist/DesignComponent.js +2 -2
- package/dist/DesignRegion.js +2 -2
- package/dist/RegionContext.js +9 -0
- package/dist/RegionContext.js.map +1 -0
- package/dist/component.types.d.ts +1 -1
- package/dist/config.d.ts +6 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/data-store.d.ts +3 -3
- package/dist/data-store.d.ts.map +1 -1
- package/dist/defaults.d.ts +106 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +67 -0
- package/dist/defaults.js.map +1 -0
- package/dist/design-data.d.ts +10 -332
- package/dist/design-data.d.ts.map +1 -1
- package/dist/design-data.js +67 -23
- package/dist/design-data.js.map +1 -1
- package/dist/design-react-core.d.ts +5 -15
- package/dist/design-react-core.d.ts.map +1 -1
- package/dist/design-react-core.js +2 -2
- package/dist/design-react.d.ts +2 -2
- package/dist/design.d.ts +2 -2
- package/dist/scapi.d.ts.map +1 -1
- package/dist/security-react.d.ts +34 -0
- package/dist/security-react.d.ts.map +1 -0
- package/dist/security-react.js +21 -0
- package/dist/security-react.js.map +1 -0
- package/dist/security.d.ts +61 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +304 -0
- package/dist/security.js.map +1 -0
- package/dist/site-context.d.ts +2 -2
- package/dist/types3.d.ts +1 -35
- package/dist/types3.d.ts.map +1 -1
- package/package.json +15 -2
- package/dist/DesignFrame.js +0 -204
- package/dist/DesignFrame.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>","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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"}
|
|
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 content → attrDef.defaultValue\n *\n * The fallback bucket is selected whole-blob style (matching SCAPI/SFRA's\n * `__data` resolution): the site-default-locale bucket if it carries any\n * content, otherwise the literal-default (\"default\") bucket. Buckets are not\n * per-key merged with each other — only the active-locale bucket layers\n * per-key on top of the chosen fallback (preserving today's locale override\n * semantics).\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, ...fallbackContent, ...localeContent }`. This keeps\n * already-deployed manifests rendering until the manifest builder starts\n * emitting `componentTypes`.\n */\nfunction composeComponentData({\n nodeData,\n literalDefaultContent,\n defaultContent,\n localeContent,\n typeDefs,\n}: {\n nodeData: Record<string, unknown> | undefined;\n literalDefaultContent: Record<string, unknown>;\n defaultContent: Record<string, unknown>;\n localeContent: Record<string, unknown>;\n typeDefs: Record<string, AttributeDefinition> | undefined;\n}): Record<string, unknown> {\n const fallbackContent = Object.keys(defaultContent).length > 0 ? defaultContent : literalDefaultContent;\n\n if (!typeDefs || Object.keys(typeDefs).length === 0) {\n return {\n ...(nodeData ?? {}),\n ...fallbackContent,\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(fallbackContent, attrId)) {\n result[attrId] = fallbackContent[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 content →\n // attribute-definition default value → key omitted. The fallback\n // is the site-default-locale bucket when present, otherwise the\n // literal-default (\"default\") bucket. Whole-blob fallback matches\n // SCAPI's `__data` resolution — the literal-default does not\n // per-key merge with the site-default-locale blob.\n // When no type definitions are available, fall back to the legacy\n // merge so existing manifests still resolve.\n const literalDefaultContent = componentInfo?.content?.default ?? {};\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 literalDefaultContent,\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 * Looks up a single content assignment in the site manifest using the\n * resolver registered for the given identifier type, returning the first\n * matching `contentId` across the resolver's ordered key list. Returns\n * `null` when the identifier type has no resolver, or no key in the list\n * has an assignment for the requested aspect type.\n */\nfunction lookupContentAssignment(\n id: string,\n identifierType: IdentifierType,\n aspectType: string,\n siteManifest?: SiteManifest | null\n): string | null {\n const lookup = ContentAssignmentResolvers.get(identifierType)?.(id, siteManifest);\n\n if (!lookup) return null;\n\n for (const key of lookup.keys) {\n const assignment = siteManifest?.contentObjectAssignments?.[aspectType]?.[lookup.objectType]?.[key];\n\n if (assignment) {\n return assignment.contentId;\n }\n }\n\n return null;\n}\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 * When the identifier type is `'product'` and no assignment is found, an\n * optional `categoryId` may be supplied as a fallback. The fallback is only\n * awaited and consulted after the product lookup misses, so callers that\n * resolve the product's category lazily (e.g. via a SCAPI request) don't\n * pay for the round trip on the happy path.\n *\n * Returns `null` if no content assignment is found for the identifier\n * (and the optional category fallback, when provided), or if the identifier\n * 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.aspectType - The aspect type to look up (e.g. `'pdp'`, `'plp'`).\n * @param options.siteManifest - The site manifest containing content assignments and category hierarchy.\n * @param options.categoryId - Optional fallback category ID (or a Promise resolving to one) used only when `identifierType` is `'product'` and the product lookup misses.\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 * await resolveDynamicPageId({ id: 'mens-shoes', identifierType: 'category', aspectType: 'plp', siteManifest });\n * // => 'page-mens-shoes-plp'\n *\n * // Inherited from parent category\n * await resolveDynamicPageId({ id: 'mens-running-shoes', identifierType: 'category', aspectType: 'plp', siteManifest });\n * // => 'page-mens-shoes-plp' (found via parent traversal)\n *\n * // Product missing but a category fallback is provided\n * await resolveDynamicPageId({\n * id: 'unknown-product',\n * identifierType: 'product',\n * aspectType: 'plp',\n * siteManifest,\n * categoryId: 'mens-running-shoes',\n * });\n * // => 'page-mens-shoes-plp'\n * ```\n */\nexport async function resolveDynamicPageId<TIdentifier extends IdentifierType = IdentifierType>({\n id,\n identifierType,\n siteManifest,\n aspectType,\n categoryId,\n}: {\n id: string;\n identifierType: TIdentifier;\n aspectType: string;\n siteManifest?: SiteManifest | null;\n categoryId?: string | Promise<string | null | undefined> | null;\n}): Promise<string | null> {\n const direct = lookupContentAssignment(id, identifierType, aspectType, siteManifest);\n\n if (direct) return direct;\n\n if (identifierType !== 'product' || categoryId == null) return null;\n\n const resolvedCategoryId = await categoryId;\n\n if (!resolvedCategoryId) return null;\n\n return lookupContentAssignment(resolvedCategoryId, 'category', aspectType, siteManifest);\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.categoryId - Optional fallback category ID (or a Promise resolving to one) used only when `identifierType` is `'product'` and the product has no content assignment for the requested aspect type. The promise is awaited lazily — the happy path never pays for it.\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 categoryId,\n locale,\n defaultLocale,\n manifestStorage,\n contextResolver,\n attrCtx,\n pruneInvisible = true,\n}: {\n id: string;\n identifierType: IdentifierType;\n aspectType?: string;\n /**\n * Fallback category ID (or a Promise resolving to one) consulted only\n * when `identifierType === 'product'` and the product has no content\n * assignment for the requested aspect type. Awaited lazily — the happy\n * path skips it.\n */\n categoryId?: string | Promise<string | null | undefined> | null;\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 = await resolveDynamicPageId({ id, identifierType, aspectType, siteManifest, categoryId });\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 // Temporary patch fix (until 26.7): manifest-driven resolution does not\n // yet handle data bindings. When the manifest declares any, bail out so\n // the caller falls back to the regular SCAPI request flow which can\n // resolve them. Tracked under W-22770039.\n if (pageManifest.context?.dataBindings?.length > 0) {\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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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { r as ShopperExperience } from "./types2.js";
|
|
2
|
-
import {
|
|
2
|
+
import { i as FrameworkAdapter, n as ComponentModule } from "./types3.js";
|
|
3
3
|
import { g as IsomorphicConfiguration } from "./index.js";
|
|
4
|
-
import {
|
|
4
|
+
import { r as RegionDecoratorProps, t as ComponentDecoratorProps } from "./component.types.js";
|
|
5
5
|
import React$1 from "react";
|
|
6
|
-
import * as
|
|
6
|
+
import * as react_jsx_runtime2 from "react/jsx-runtime";
|
|
7
7
|
|
|
8
8
|
//#region src/design/react/core/PageDesignerProvider.d.ts
|
|
9
9
|
type PageDesignerContextType = {
|
|
@@ -49,25 +49,15 @@ declare function PageDesignerPageMetadataProvider({
|
|
|
49
49
|
children
|
|
50
50
|
}: React.PropsWithChildren<{
|
|
51
51
|
page: ShopperExperience.schemas['Page'];
|
|
52
|
-
}>):
|
|
52
|
+
}>): react_jsx_runtime2.JSX.Element;
|
|
53
53
|
//#endregion
|
|
54
54
|
//#region src/design/react/core/RegionContext.d.ts
|
|
55
55
|
interface RegionContextType {
|
|
56
56
|
regionId: string;
|
|
57
57
|
contentLinkUuids: string[];
|
|
58
58
|
}
|
|
59
|
-
declare const RegionContext: React$1.Context<RegionContextType | null>;
|
|
60
59
|
declare const useRegionContext: () => RegionContextType | null;
|
|
61
60
|
//#endregion
|
|
62
|
-
//#region src/design/react/core/ComponentContext.d.ts
|
|
63
|
-
interface ComponentContextType {
|
|
64
|
-
componentId: string;
|
|
65
|
-
name?: string;
|
|
66
|
-
contentLinkUuid?: string;
|
|
67
|
-
}
|
|
68
|
-
declare const ComponentContext: React$1.Context<ComponentContextType | null>;
|
|
69
|
-
declare const useComponentContext: () => ComponentContextType | null;
|
|
70
|
-
//#endregion
|
|
71
61
|
//#region src/design/react/core/ComponentDecorator.d.ts
|
|
72
62
|
/**
|
|
73
63
|
* Creates a higher-order component that wraps React components with design-time functionality.
|
|
@@ -111,5 +101,5 @@ declare class ReactAdapter<TProps> implements FrameworkAdapter<TProps, ReactDesi
|
|
|
111
101
|
*/
|
|
112
102
|
declare function createReactAdapter<TProps>(): ReactAdapter<TProps>;
|
|
113
103
|
//#endregion
|
|
114
|
-
export {
|
|
104
|
+
export { PageDesignerPageMetadataProvider, PageDesignerProvider, type ReactDesignComponentType, createReactAdapter, createReactComponentDesignDecorator, createReactRegionDesignDecorator, usePageDesignerMode, useRegionContext };
|
|
115
105
|
//# sourceMappingURL=design-react-core.d.ts.map
|