diff --git a/.changeset/wet-garlics-trade.md b/.changeset/wet-garlics-trade.md new file mode 100644 index 0000000000..bdeae0f906 --- /dev/null +++ b/.changeset/wet-garlics-trade.md @@ -0,0 +1,6 @@ +--- +'@shopify/hydrogen': patch +'@shopify/hydrogen-react': patch +--- + +Add utility functions `decodeEncodedVariant` and `isOptionValueCombinationInEncodedVariant` for parsing `product.encodedVariantExistence` and `product.encodedVariantAvailability` fields. diff --git a/packages/hydrogen-react/src/index.ts b/packages/hydrogen-react/src/index.ts index 5c498275af..b6d0e9fcf2 100644 --- a/packages/hydrogen-react/src/index.ts +++ b/packages/hydrogen-react/src/index.ts @@ -50,6 +50,10 @@ export {useLoadScript} from './load-script.js'; export {MediaFile} from './MediaFile.js'; export {ModelViewer} from './ModelViewer.js'; export {Money} from './Money.js'; +export { + decodeEncodedVariant, + isOptionValueCombinationInEncodedVariant, +} from './optionValueDecoder.js'; export {type ParsedMetafields, parseMetafield} from './parse-metafield.js'; export {ProductPrice} from './ProductPrice.js'; export {ProductProvider, useProduct} from './ProductProvider.js'; diff --git a/packages/hydrogen-react/src/optionValueDecoder.decodeEncodedVariant.doc.ts b/packages/hydrogen-react/src/optionValueDecoder.decodeEncodedVariant.doc.ts new file mode 100644 index 0000000000..924882b497 --- /dev/null +++ b/packages/hydrogen-react/src/optionValueDecoder.decodeEncodedVariant.doc.ts @@ -0,0 +1,39 @@ +import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'; + +const data: ReferenceEntityTemplateSchema = { + name: 'decodeEncodedVariant', + category: 'utilities', + isVisualComponent: false, + related: [ + { + name: 'isOptionValueCombinationInEncodedVariant', + type: 'utility', + url: '/docs/api/hydrogen/2024-10/utilities/isOptionValueCombinationInEncodedVariant', + }, + ], + description: + 'Decodes an encoded option value string into an array of option value combinations.', + type: 'utility', + defaultExample: { + description: 'Decode an encoded option value string', + codeblock: { + tabs: [ + { + title: 'JavaScript', + code: './optionValueDecoder.decodeEncodedVariant.example.js', + language: 'js', + }, + ], + title: 'Example code', + }, + }, + definitions: [ + { + title: 'Props', + type: 'DecodeEncodedVariantGeneratedType', + description: '', + }, + ], +}; + +export default data; diff --git a/packages/hydrogen-react/src/optionValueDecoder.decodeEncodedVariant.example.js b/packages/hydrogen-react/src/optionValueDecoder.decodeEncodedVariant.example.js new file mode 100644 index 0000000000..7de9cb3ee2 --- /dev/null +++ b/packages/hydrogen-react/src/optionValueDecoder.decodeEncodedVariant.example.js @@ -0,0 +1,34 @@ +import {decodeEncodedVariant} from '@shopify/hydrogen-react'; + +// product.options = [ +// { +// name: 'Color', +// optionValues: [ +// {name: 'Red'}, +// {name: 'Blue'}, +// {name: 'Green'}, +// ] +// }, +// { +// name: 'Size', +// optionValues: [ +// {name: 'S'}, +// {name: 'M'}, +// {name: 'L'}, +// ] +// } +// ] + +const encodedVariantAvailability = 'v1_0:0-2,1:2,'; + +const decodedVariantAvailability = decodeEncodedVariant( + encodedVariantAvailability, +); + +// decodedVariantAvailability +// { +// [0,0], // Red, S +// [0,1], // Red, M +// [0,2], // Red, L +// [1,2] // Blue, L +// } diff --git a/packages/hydrogen-react/src/optionValueDecoder.isOptionValueCombinationInEncodedVariant.doc.ts b/packages/hydrogen-react/src/optionValueDecoder.isOptionValueCombinationInEncodedVariant.doc.ts new file mode 100644 index 0000000000..0efecd40c7 --- /dev/null +++ b/packages/hydrogen-react/src/optionValueDecoder.isOptionValueCombinationInEncodedVariant.doc.ts @@ -0,0 +1,40 @@ +import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'; + +const data: ReferenceEntityTemplateSchema = { + name: 'isOptionValueCombinationInEncodedVariant', + category: 'utilities', + isVisualComponent: false, + related: [ + { + name: 'decodeEncodedVariant', + type: 'utility', + url: '/docs/api/hydrogen/2024-10/utilities/decodeEncodedVariant', + }, + ], + description: ` + Determines whether an option value combination is present in an encoded option value string.\n\n\`targetOptionValueCombination\` - Indices of option values to look up in the encoded option value string. A partial set of indices may be passed to determine whether a node or any children is present. For example, if a product has 3 options, passing \`[0]\` will return true if any option value combination for the first option's option value is present in the encoded string. + `, + type: 'utility', + defaultExample: { + description: 'Check if option value is in encoding', + codeblock: { + tabs: [ + { + title: 'JavaScript', + code: './optionValueDecoder.isOptionValueCombinationInEncodedVariant.example.js', + language: 'js', + }, + ], + title: 'Example code', + }, + }, + definitions: [ + { + title: 'Props', + type: 'IsOptionValueCombinationInEncodedVariantForDocs', + description: '', + }, + ], +}; + +export default data; diff --git a/packages/hydrogen-react/src/optionValueDecoder.isOptionValueCombinationInEncodedVariant.example.js b/packages/hydrogen-react/src/optionValueDecoder.isOptionValueCombinationInEncodedVariant.example.js new file mode 100644 index 0000000000..19ddf6da41 --- /dev/null +++ b/packages/hydrogen-react/src/optionValueDecoder.isOptionValueCombinationInEncodedVariant.example.js @@ -0,0 +1,45 @@ +import {isOptionValueCombinationInEncodedVariant} from '@shopify/hydrogen-react'; + +// product.options = [ +// { +// name: 'Color', +// optionValues: [ +// {name: 'Red'}, +// {name: 'Blue'}, +// {name: 'Green'}, +// ] +// }, +// { +// name: 'Size', +// optionValues: [ +// {name: 'S'}, +// {name: 'M'}, +// {name: 'L'}, +// ] +// } +// ] +const encodedVariantExistence = 'v1_0:0-1,1:2,'; + +// For reference: decoded encodedVariantExistence +// { +// [0,0], // Red, S +// [0,1], // Red, M +// [1,2] // Blue, L +// } + +// Returns true since there are variants exist for [Red] +isOptionValueCombinationInEncodedVariant([0], encodedVariantExistence); // true + +isOptionValueCombinationInEncodedVariant([0, 0], encodedVariantExistence); // true +isOptionValueCombinationInEncodedVariant([0, 1], encodedVariantExistence); // true +isOptionValueCombinationInEncodedVariant([0, 2], encodedVariantExistence); // false - no variant exist for [Red, L] + +// Returns true since there is a variant exist for [Blue] +isOptionValueCombinationInEncodedVariant([1], encodedVariantExistence); // true + +isOptionValueCombinationInEncodedVariant([1, 0], encodedVariantExistence); // false - no variant exist for [Blue, S] +isOptionValueCombinationInEncodedVariant([1, 1], encodedVariantExistence); // false - no variant exist for [Blue, M] +isOptionValueCombinationInEncodedVariant([1, 2], encodedVariantExistence); // true + +// Returns false since there is no variant exist for [Green] +isOptionValueCombinationInEncodedVariant([2], encodedVariantExistence); // false diff --git a/packages/hydrogen-react/src/optionValueDecoder.test.ts b/packages/hydrogen-react/src/optionValueDecoder.test.ts new file mode 100644 index 0000000000..420c1e4aef --- /dev/null +++ b/packages/hydrogen-react/src/optionValueDecoder.test.ts @@ -0,0 +1,187 @@ +import {describe, expect, it} from 'vitest'; +import { + decodeEncodedVariant, + isOptionValueCombinationInEncodedVariant, +} from './optionValueDecoder.js'; + +describe('isOptionValueCombinationInEncodedVariant', () => { + it('returns true when target option values are present in encoded option values', () => { + const MOCK_ENCODED_OPTION_VALUES = 'v1_0:0:0,,1:1:1,,2:2:2,,'; + + expect( + isOptionValueCombinationInEncodedVariant( + [0, 0, 0], + MOCK_ENCODED_OPTION_VALUES, + ), + ).toBe(true); + }); + + it('returns true when a partial target option value is present in encoded option values', () => { + const MOCK_ENCODED_OPTION_VALUES = 'v1_0:0:0,,1:1:1,,2:2:2,,'; + + expect( + isOptionValueCombinationInEncodedVariant([0], MOCK_ENCODED_OPTION_VALUES), + ).toBe(true); + + expect( + isOptionValueCombinationInEncodedVariant( + [2, 2], + MOCK_ENCODED_OPTION_VALUES, + ), + ).toBe(true); + }); + + it('returns false when target option values are not present in encoded option values', () => { + const MOCK_ENCODED_OPTION_VALUES = 'v1_0:0:0,,1:1:1,,2:2:2,,'; + + expect( + isOptionValueCombinationInEncodedVariant( + [0, 0, 1], + MOCK_ENCODED_OPTION_VALUES, + ), + ).toBe(false); + }); + + it('returns false if no target option values are passed', () => { + const MOCK_ENCODED_OPTION_VALUES = 'v1_0:0:0,,1:1:1,,2:2:2,,'; + + expect( + isOptionValueCombinationInEncodedVariant([], MOCK_ENCODED_OPTION_VALUES), + ).toBe(false); + }); +}); + +describe('decodeEncodedVariant', () => { + describe('v1', () => { + const VERSION_PREFIX = 'v1_'; + + it('it correctly decodes a set of 1-dimensional arrays with no gaps in the number sequence', () => { + expect(decodeEncodedVariant(`${VERSION_PREFIX}0-9`)).toStrictEqual([ + [0], + [1], + [2], + [3], + [4], + [5], + [6], + [7], + [8], + [9], + ]); + }); + + it('it correctly decodes a set of 1-dimensional arrays with gaps in the number sequence', () => { + expect( + decodeEncodedVariant(`${VERSION_PREFIX}0-2 4-6 8-9`), + ).toStrictEqual([[0], [1], [2], [4], [5], [6], [8], [9]]); + }); + + it('it correctly decodes a set of 2-dimensional arrays with no gaps in the number sequence', () => { + expect( + decodeEncodedVariant(`${VERSION_PREFIX}0:0-2,1:0-2,2:0-2,`), + ).toStrictEqual([ + [0, 0], + [0, 1], + [0, 2], + [1, 0], + [1, 1], + [1, 2], + [2, 0], + [2, 1], + [2, 2], + ]); + }); + + it('it correctly decodes a set of 2-dimensional arrays with gaps in the number sequence', () => { + expect( + decodeEncodedVariant(`${VERSION_PREFIX}0:0-1,1:0 2,2:1-2,`), + ).toStrictEqual([ + [0, 0], + [0, 1], + [1, 0], + [1, 2], + [2, 1], + [2, 2], + ]); + }); + + it('it correctly decodes a set of 3-dimensional arrays with no gaps in the number sequence', () => { + const output = generateCombinations(3, 3); + expect( + decodeEncodedVariant( + `${VERSION_PREFIX}0:0:0-2,1:0-2,2:0-2,,1:0:0-2,1:0-2,2:0-2,,2:0:0-2,1:0-2,2:0-2,,`, + ), + ).toStrictEqual(output); + }); + + it('it correctly decodes a set of 3-dimensional arrays with gaps in the number sequence', () => { + const output = generateCombinations(3, 3, [ + [1, 0, 1], + [2, 2, 2], + [0, 2, 2], + ]); + expect( + decodeEncodedVariant( + `${VERSION_PREFIX}0:0:0-2,1:0-2,2:0-1,,1:0:0 2,1:0-2,2:0-2,,2:0:0-2,1:0-2,2:0-1,`, + ), + ).toStrictEqual(output); + }); + + it('it correctly decodes a set of 4-dimensional arrays with no gaps in the number sequence', () => { + const output = generateCombinations(4, 3); + expect( + decodeEncodedVariant( + `${VERSION_PREFIX}"0:0:0:0-2,1:0-2,2:0-2,,1:0:0-2,1:0-2,2:0-2,,2:0:0-2,1:0-2,2:0-2,,,1:0:0:0-2,1:0-2,2:0-2,,1:0:0-2,1:0-2,2:0-2,,2:0:0-2,1:0-2,2:0-2,,,2:0:0:0-2,1:0-2,2:0-2,,1:0:0-2,1:0-2,2:0-2,,2:0:0-2,1:0-2,2:0-2,,,"`, + ), + ).toStrictEqual(output); + }); + + it('it correctly decodes a set of 4-dimensional arrays with gaps in the number sequence', () => { + const output = generateCombinations(4, 3, [ + [1, 0, 1, 0], + [2, 2, 2, 0], + [0, 2, 2, 1], + ]); + expect( + decodeEncodedVariant( + `${VERSION_PREFIX}0:0:0:0-2,1:0-2,2:0-2,,1:0:0-2,1:0-2,2:0-2,,2:0:0-2,1:0-2,2:0 2,,,1:0:0:0-2,1:1-2,2:0-2,,1:0:0-2,1:0-2,2:0-2,,2:0:0-2,1:0-2,2:0-2,,,2:0:0:0-2,1:0-2,2:0-2,,1:0:0-2,1:0-2,2:0-2,,2:0:0-2,1:0-2,2:1-2,,,`, + ), + ).toStrictEqual(output); + }); + }); +}); + +// generate all possible option value combos of a given depth and width +function generateCombinations( + depth: number, + width: number, + exclusions: number[][] = [], +): number[][] { + const input: number[][] = []; + + function isInExclusions(array: number[]): boolean { + return exclusions.some( + (exclusion) => + exclusion.length === array.length && + exclusion.every((value, index) => value === array[index]), + ); + } + + function generate(currentArray: number[]): void { + if (currentArray.length === depth) { + if (!isInExclusions(currentArray)) { + input.push([...currentArray]); + } + return; + } + + for (let i = 0; i < width; i++) { + currentArray.push(i); + generate(currentArray); + currentArray.pop(); + } + } + + generate([]); + return input; +} diff --git a/packages/hydrogen-react/src/optionValueDecoder.ts b/packages/hydrogen-react/src/optionValueDecoder.ts new file mode 100644 index 0000000000..65857d1748 --- /dev/null +++ b/packages/hydrogen-react/src/optionValueDecoder.ts @@ -0,0 +1,189 @@ +/** + * This file provides utility functions for determining whether or not an option value combination is present in an encoded option value string. + * + * In V1 of the encoding strategy, option value arrays are encoded as a trie with the following rules: + * - `:` `,` ` ` and `-` are control characters. + * - `:` indicates a new option. ex: 0:1 indicates value 0 for the option in position 1, value 1 for the option in position 2. + * - `,` indicates the end of a repeated prefix, mulitple consecutive commas indicate the end of multiple repeated prefixes. + * - ` ` indicates a gap in the sequence of option values. ex: `0 4` indicates option values in position 0 and 4 are present. + * - `-` indicates a continuous range of option values. ex: `0 1-3 4`. Ranges are only present encoded in the final option value position, so for example the trie for the set [[0,0,0],[0,0,1], ..., [0,2,2]] will be structured as `0:0:0-2,1:0-2,2:0-2`, not `0:0-2:0-2`. + */ + +import {Product} from './storefront-api-types.js'; + +const OPTION_VALUE_SEPARATOR = ','; + +const V1_CONTROL_CHARS = { + OPTION: ':', + END_OF_PREFIX: ',', + SEQUENCE_GAP: ' ', + RANGE: '-', +}; + +export type IsOptionValueCombinationInEncodedVariant = ( + targetOptionValueCombination: number[], + encodedVariantField: string, +) => boolean; + +/** + * Determine whether an option value combination is present in an encoded option value string. Function is memoized by encodedVariantField. + * + * @param targetOptionValueCombination - Indices of option values to look up in the encoded option value string. A partial set of indices may be passed to determine whether a node or any children is present. For example, if a product has 3 options, passing [0] will return true if any option value combination for the first option's option value is present in the encoded string. + * @param encodedVariantField - Encoded option value string from the Storefront API, e.g. [product.encodedVariantExistence](/docs/api/storefront/2024-10/objects/Product#field-encodedvariantexistence) or [product.encodedVariantAvailability](/docs/api/storefront/2024-10/objects/Product#field-encodedvariantavailability) + * @returns - True if a full or partial targetOptionValueIndices is present in the encoded option value string, false otherwise. + */ +export const isOptionValueCombinationInEncodedVariant: IsOptionValueCombinationInEncodedVariant = + ((): IsOptionValueCombinationInEncodedVariant => { + const decodedOptionValues = new Map>(); + + return function ( + targetOptionValueCombination: number[], + encodedVariantField: string, + ): boolean { + if (targetOptionValueCombination.length === 0) { + return false; + } + + if (!decodedOptionValues.has(encodedVariantField)) { + const decodedOptionValuesSet = new Set(); + + for (const optionValue of decodeEncodedVariant(encodedVariantField)) { + // add the complete option value to the decoded option values set + decodedOptionValuesSet.add(optionValue.join(OPTION_VALUE_SEPARATOR)); + + // add all composite parts of the option value to the decoded option values set. e.g. if the option value is [0,1,2], add "0", "0,1", "0,1,2" + for (let i = 0; i < optionValue.length; i++) { + decodedOptionValuesSet.add( + optionValue.slice(0, i + 1).join(OPTION_VALUE_SEPARATOR), + ); + } + } + + decodedOptionValues.set(encodedVariantField, decodedOptionValuesSet); + } + + return Boolean( + decodedOptionValues + .get(encodedVariantField) + ?.has(targetOptionValueCombination.join(OPTION_VALUE_SEPARATOR)), + ); + }; + })(); + +type EncodedVariantField = + | Product['encodedVariantAvailability'] + | Product['encodedVariantExistence']; +type DecodedOptionValues = number[][]; + +/** + * For an encoded option value string, decode into option value combinations. Entries represent a valid combination formatted as an array of option value positions. + * @param encodedVariantField - Encoded option value string from the Storefront API, e.g. [product.encodedVariantExistence](/docs/api/storefront/2024-10/objects/Product#field-encodedvariantexistence) or [product.encodedVariantAvailability](/docs/api/storefront/2024-10/objects/Product#field-encodedvariantavailability) + * @returns Decoded option value combinations + */ +export function decodeEncodedVariant( + encodedVariantField: EncodedVariantField, +): DecodedOptionValues { + if (!encodedVariantField) return []; + + if (encodedVariantField.startsWith('v1_')) { + return v1Decoder(stripVersion(encodedVariantField)); + } + + throw new Error('Unsupported option value encoding'); +} + +const stripVersion: (encodedVariantField: string) => string = ( + encodedVariantField: string, +) => encodedVariantField.replace(/^v1_/, ''); + +/** + * We encode an array of arrays representing variants, expressed in terms of options and option values, as a trie. + * + * This encoding strategy allows extremely large numbers of variants to be expressed in an extremely compact data structure. + * + * Integers represent option and values, so [0,0,0] represents option_value at array index 0 for the options at array indexes 0, 1 and 2 + * + * `:`, `,`, ` ` and `-` are control characters. + * `:` indicates a new option + * `,` indicates the end of a repeated prefix, mulitple consecutive commas indicate the end of multiple repeated prefixes. + * ` ` indicates a gap in the sequence of option values + * `-` indicates a continuous range of option values + * + * Encoding process: + * + * example input array: [[0,0,0], [0,1,0], [0,1,1], [1,0,0], [1,0,1], [1,1,1], [2,0,1], [2,1,0]] + * + * step 1: encode as string: "0:0:0,0:1:0,0:1:1,1:0:0,1:0:1,1:1:1,2:0:1,2:1:0," + * step 2: combine nodes that share a prefix: "0:0:0,0:1:0 1,1:0:0 1,1:1:1,2:0:1,2:1:0," + * step 3: encode data as a trie so no prefixes need to be repeated: "0:0:0,1:0 1,,1:0:0 1,1:1,,2:0:1,1:0,," + * step 4: since the options are sorted, use a dash to express ranges: "0:0:0,1:0-1,,1:0:0-1,1:1,,2:0:1,1:0,," + */ +function v1Decoder(encodedVariantField: string): number[][] { + const tokenizer = /[ :,-]/g; + let index = 0; + let token: RegExpExecArray | null; + const options: number[][] = []; + const currentOptionValue: number[] = []; + let depth = 0; + let rangeStart: number | null = null; + + // iterate over control characters + while ((token = tokenizer.exec(encodedVariantField))) { + const operation = token[0]; + const optionValueIndex = + Number.parseInt(encodedVariantField.slice(index, token.index)) || 0; + + if (rangeStart !== null) { + // If a range has been started, iterate over the range and add each option value to the list of options + // - `rangeStart` is set if the last control char was a dash, e.g. `0` for 0-2. It represents the numeric option value position for the start of the range. + // - `optionValueIndex` is the numeric option value position for the end of the range + for (; rangeStart < optionValueIndex; rangeStart++) { + currentOptionValue[depth] = rangeStart; + options.push([...currentOptionValue]); + } + // indicates the range has been processed + rangeStart = null; + } + + currentOptionValue[depth] = optionValueIndex; + + if (operation === V1_CONTROL_CHARS.RANGE) { + // dash operation indicates we are in a range. e.g. 0-2 means option values 0, 1, 2 + rangeStart = optionValueIndex; + } else if (operation === V1_CONTROL_CHARS.OPTION) { + // colon operation indicates that we are moving down to the next layer of option values. e.g. 0:0:0-2 means we traverse down from option1 to option3 and represents [[0,0,0], [0,0,1], [0,0,2]] + depth++; + } else { + if ( + operation === V1_CONTROL_CHARS.SEQUENCE_GAP || + (operation === V1_CONTROL_CHARS.END_OF_PREFIX && + encodedVariantField[token.index - 1] !== + V1_CONTROL_CHARS.END_OF_PREFIX) + ) { + // add the current option value to the list of options if we hit a gap in our sequence or we are at the end of our depth and need to move back up + options.push([...currentOptionValue]); + } + if (operation === V1_CONTROL_CHARS.END_OF_PREFIX) { + // go up an option level, trash the last item in currentOptionValue + currentOptionValue.pop(); + depth--; + } + } + index = tokenizer.lastIndex; + } + + // Because we iterate over control characters and the range processing happens in the while, + // if the last control char is a range we need to manually add the final range to the option list. + const lastRangeStartIndex = encodedVariantField.lastIndexOf('-'); + if (rangeStart != null && lastRangeStartIndex > 0) { + const finalValueIndex = parseInt( + encodedVariantField.substring(lastRangeStartIndex + 1), + ); + for (; rangeStart <= finalValueIndex; rangeStart++) { + currentOptionValue[depth] = rangeStart; + options.push([...currentOptionValue]); + } + } + + return options; +} diff --git a/packages/hydrogen/docs/copy-hydrogen-react-docs.cjs b/packages/hydrogen/docs/copy-hydrogen-react-docs.cjs index dfbdb14bfd..a051b553d4 100644 --- a/packages/hydrogen/docs/copy-hydrogen-react-docs.cjs +++ b/packages/hydrogen/docs/copy-hydrogen-react-docs.cjs @@ -22,6 +22,8 @@ const docsToCopy = [ 'parseGid', 'Storefront Schema', 'Storefront API Types', + 'decodeEncodedVariant', + 'isOptionValueCombinationInEncodedVariant', ]; async function copyFiles() { diff --git a/packages/hydrogen/src/index.ts b/packages/hydrogen/src/index.ts index 13d360633e..3f65a3fc93 100644 --- a/packages/hydrogen/src/index.ts +++ b/packages/hydrogen/src/index.ts @@ -144,6 +144,8 @@ export { useMoney, useShopifyCookies, Video, + isOptionValueCombinationInEncodedVariant, + decodeEncodedVariant, } from '@shopify/hydrogen-react'; export {RichText} from './RichText'; diff --git a/packages/hydrogen/src/product/VariantSelector.ts b/packages/hydrogen/src/product/VariantSelector.ts index 5566b73beb..98ca03fc21 100644 --- a/packages/hydrogen/src/product/VariantSelector.ts +++ b/packages/hydrogen/src/product/VariantSelector.ts @@ -27,7 +27,7 @@ export type VariantOptionValue = { type VariantSelectorProps = { /** The product handle for all of the variants */ handle: string; - /** Product options from the [Storefront API](/docs/api/storefront/2024-07/objects/ProductOption). Make sure both `name` and `values` are apart of your query. */ + /** Product options from the [Storefront API](/docs/api/storefront/2024-07/objects/ProductOption). Make sure both `name` and `values` are a part of your query. */ options: Array> | undefined; /** Product variants from the [Storefront API](/docs/api/storefront/2024-07/objects/ProductVariant). You only need to pass this prop if you want to show product availability. If a product option combination is not found within `variants`, it is assumed to be available. Make sure to include `availableForSale` and `selectedOptions.name` and `selectedOptions.value`. */ variants?: