@shopify/cli 3.72.1 → 3.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/dist/assets/hydrogen/starter/CHANGELOG.md +814 -0
  2. package/dist/assets/hydrogen/starter/app/components/Aside.tsx +1 -1
  3. package/dist/assets/hydrogen/starter/app/components/PaginatedResourceSection.tsx +3 -4
  4. package/dist/assets/hydrogen/starter/app/components/ProductForm.tsx +113 -44
  5. package/dist/assets/hydrogen/starter/app/components/SearchResults.tsx +8 -3
  6. package/dist/assets/hydrogen/starter/app/components/SearchResultsPredictive.tsx +6 -5
  7. package/dist/assets/hydrogen/starter/app/lib/variants.ts +3 -3
  8. package/dist/assets/hydrogen/starter/app/routes/account_.logout.tsx +1 -1
  9. package/dist/assets/hydrogen/starter/app/routes/cart.tsx +9 -16
  10. package/dist/assets/hydrogen/starter/app/routes/collections.$handle.tsx +1 -10
  11. package/dist/assets/hydrogen/starter/app/routes/collections.all.tsx +1 -10
  12. package/dist/assets/hydrogen/starter/app/routes/products.$handle.tsx +50 -119
  13. package/dist/assets/hydrogen/starter/app/routes/search.tsx +42 -38
  14. package/dist/assets/hydrogen/starter/app/styles/app.css +25 -1
  15. package/dist/assets/hydrogen/starter/guides/predictiveSearch/predictiveSearch.md +23 -20
  16. package/dist/assets/hydrogen/starter/guides/search/search.md +27 -25
  17. package/dist/assets/hydrogen/starter/package.json +3 -3
  18. package/dist/assets/hydrogen/starter/storefrontapi.generated.d.ts +177 -194
  19. package/dist/assets/hydrogen/tailwind/tailwind.css +1 -1
  20. package/dist/{chokidar-OESTCX4H.js → chokidar-5LLC6S6D.js} +47 -90
  21. package/dist/{chunk-VSLR7ET4.js → chunk-25IMI7TH.js} +8 -46
  22. package/dist/{chunk-NCKQIOV4.js → chunk-3HBRMIPY.js} +28 -50
  23. package/dist/{chunk-4DZKSJ2R.js → chunk-3OXCI3HX.js} +1402 -1989
  24. package/dist/{chunk-NX2KGZ2W.js → chunk-5OFJTURZ.js} +4 -4
  25. package/dist/{chunk-YITJ52BH.js → chunk-65GSOZHL.js} +7 -7
  26. package/dist/{chunk-5BCHJBAS.js → chunk-66HLUVKV.js} +4 -4
  27. package/dist/{chunk-J6WHIPSP.js → chunk-6PEBJVBW.js} +3 -3
  28. package/dist/{chunk-XVNW332R.js → chunk-75LV6AQS.js} +6 -10
  29. package/dist/{chunk-4ERNAW5E.js → chunk-7RKRCUFA.js} +16518 -24140
  30. package/dist/{chunk-FGSRYMBN.js → chunk-AFPOP3K5.js} +67 -104
  31. package/dist/{chunk-2HGYYNE5.js → chunk-B5EXYCV3.js} +5 -9
  32. package/dist/{chunk-NB4NLOEJ.js → chunk-BUFIEXZ5.js} +11 -20
  33. package/dist/{chunk-EQQFJ434.js → chunk-CEIZT2W3.js} +4 -4
  34. package/dist/{chunk-522OB3EU.js → chunk-CFIKVUNW.js} +2 -2
  35. package/dist/{chunk-KYB6A4PE.js → chunk-CRHXI6PS.js} +13 -23
  36. package/dist/{chunk-QBE6MS4X.js → chunk-ELTOJBOJ.js} +4 -5
  37. package/dist/{chunk-NUP5TATA.js → chunk-EZQWZ57B.js} +2 -2
  38. package/dist/{chunk-HAURZWLZ.js → chunk-F7D333WQ.js} +3 -3
  39. package/dist/{chunk-UBB7JKND.js → chunk-G2ZZKGSV.js} +2 -2
  40. package/dist/{chunk-CBBS4CV7.js → chunk-G5R6YD27.js} +8 -13
  41. package/dist/{chunk-REXJJC44.js → chunk-GDLROW57.js} +3 -3
  42. package/dist/{chunk-TQTXTCOI.js → chunk-I25PGLBO.js} +3 -3
  43. package/dist/{chunk-OXKHBIW7.js → chunk-IG5SOACB.js} +29 -34
  44. package/dist/{chunk-2FXFMB2U.js → chunk-IRHYYIN7.js} +4 -4
  45. package/dist/{chunk-OWLPHMUA.js → chunk-J673ZU5S.js} +4 -4
  46. package/dist/{chunk-U2VX4Q6V.js → chunk-JORKLY7M.js} +68 -49
  47. package/dist/{chunk-K6Y4FYT5.js → chunk-K7HGDAI4.js} +17 -20
  48. package/dist/{chunk-MTXNEHKM.js → chunk-KF2D6QHZ.js} +39 -68
  49. package/dist/{chunk-QBDNLBUG.js → chunk-KMWARALD.js} +5 -5
  50. package/dist/{chunk-E7MQ72JO.js → chunk-KTNFE44J.js} +111 -84
  51. package/dist/{chunk-BYPQXSAL.js → chunk-KUM3DVPF.js} +17 -13
  52. package/dist/{chunk-B2HZKGLW.js → chunk-KZBL6BQ7.js} +4 -4
  53. package/dist/{chunk-WNDN5FAY.js → chunk-MHUINF7I.js} +3 -3
  54. package/dist/{chunk-YDFZISQ3.js → chunk-NFQLKURK.js} +41 -68
  55. package/dist/{chunk-7JVASAC6.js → chunk-O77L7CCL.js} +4 -4
  56. package/dist/{chunk-SNOECVP4.js → chunk-OAZFIMJ3.js} +2 -2
  57. package/dist/{chunk-OYHBLPKV.js → chunk-OE3IXTC5.js} +38 -60
  58. package/dist/{chunk-HLKMRAT5.js → chunk-ONOLOXLM.js} +5 -5
  59. package/dist/{chunk-POZ5MGPT.js → chunk-PKR7KJ6P.js} +2 -3
  60. package/dist/{chunk-YG5QRMVT.js → chunk-PRWEHR2C.js} +3 -3
  61. package/dist/{chunk-72RTT46Y.js → chunk-QNK2EAZ3.js} +5 -5
  62. package/dist/{chunk-3G3CTE4U.js → chunk-RCA7PFH4.js} +4 -4
  63. package/dist/{chunk-OG2XEFVO.js → chunk-RK7JAMCI.js} +6 -6
  64. package/dist/{chunk-QRNLG3CD.js → chunk-SANP6FPA.js} +6 -6
  65. package/dist/{chunk-OJOHMVV7.js → chunk-SBPFWO4S.js} +294 -448
  66. package/dist/{chunk-O5K4AU7Q.js → chunk-SHWOPMLQ.js} +3 -4
  67. package/dist/{chunk-6Z7SI3ZW.js → chunk-SUUVDRTQ.js} +6 -7
  68. package/dist/{chunk-HHAGWNRI.js → chunk-TEHNKBLD.js} +6 -7
  69. package/dist/{chunk-XHR6UC4Y.js → chunk-VPNXQGG6.js} +4 -4
  70. package/dist/{chunk-MWLLV4UV.js → chunk-X5FJXK25.js} +9 -9
  71. package/dist/{chunk-PMUQTGZJ.js → chunk-X7YTIMNN.js} +4 -6
  72. package/dist/{chunk-PC7RDKJJ.js → chunk-XAGT2UNE.js} +3 -3
  73. package/dist/{chunk-7Q3MMWAC.js → chunk-XE5EOEBL.js} +2 -2
  74. package/dist/{chunk-VVD57NWZ.js → chunk-YP7WU5EU.js} +5 -5
  75. package/dist/{chunk-G2WBIWNF.js → chunk-YPTEMDFR.js} +5 -5
  76. package/dist/{chunk-4PXLANQP.js → chunk-ZENVITME.js} +4 -4
  77. package/dist/{chunk-HSTSRNLJ.js → chunk-ZX3L2JKV.js} +64 -86
  78. package/dist/cli/commands/auth/logout.js +28 -28
  79. package/dist/cli/commands/auth/logout.test.js +30 -30
  80. package/dist/cli/commands/cache/clear.js +27 -27
  81. package/dist/cli/commands/debug/command-flags.js +27 -27
  82. package/dist/cli/commands/docs/generate.js +27 -27
  83. package/dist/cli/commands/docs/generate.test.js +28 -28
  84. package/dist/cli/commands/help.js +27 -27
  85. package/dist/cli/commands/kitchen-sink/async.js +28 -28
  86. package/dist/cli/commands/kitchen-sink/async.test.js +29 -29
  87. package/dist/cli/commands/kitchen-sink/index.js +30 -30
  88. package/dist/cli/commands/kitchen-sink/index.test.js +31 -31
  89. package/dist/cli/commands/kitchen-sink/prompts.js +28 -28
  90. package/dist/cli/commands/kitchen-sink/prompts.test.js +29 -29
  91. package/dist/cli/commands/kitchen-sink/static.js +28 -28
  92. package/dist/cli/commands/kitchen-sink/static.test.js +29 -29
  93. package/dist/cli/commands/notifications/generate.js +28 -28
  94. package/dist/cli/commands/notifications/list.js +28 -28
  95. package/dist/cli/commands/search.js +28 -28
  96. package/dist/cli/commands/upgrade.js +28 -28
  97. package/dist/cli/commands/upgrade.test.js +3 -3
  98. package/dist/cli/commands/version.js +28 -28
  99. package/dist/cli/commands/version.test.js +29 -29
  100. package/dist/cli/services/commands/notifications.js +21 -21
  101. package/dist/cli/services/commands/search.js +15 -15
  102. package/dist/cli/services/commands/search.test.js +16 -16
  103. package/dist/cli/services/commands/version.js +16 -16
  104. package/dist/cli/services/commands/version.test.js +18 -18
  105. package/dist/cli/services/kitchen-sink/async.js +15 -15
  106. package/dist/cli/services/kitchen-sink/prompts.js +15 -15
  107. package/dist/cli/services/kitchen-sink/static.js +15 -15
  108. package/dist/cli/services/upgrade.js +17 -17
  109. package/dist/cli/services/upgrade.test.js +20 -20
  110. package/dist/configs/all.yml +12 -0
  111. package/dist/configs/recommended.yml +15 -0
  112. package/dist/configs/theme-app-extension.yml +3 -0
  113. package/dist/{custom-oclif-loader-MUOL3HPQ.js → custom-oclif-loader-V3IB4SYZ.js} +15 -15
  114. package/dist/data/latest.json +1 -1
  115. package/dist/data/manifest_theme.json +1 -0
  116. package/dist/data/preset.json +66 -0
  117. package/dist/data/preset_blocks.json +20 -13
  118. package/dist/data/section.json +1 -15
  119. package/dist/data/setting.json +1 -1
  120. package/dist/data/theme_block.json +1 -15
  121. package/dist/{del-K5ZJEWTD.js → del-P2RS6GN2.js} +9 -9
  122. package/dist/{devtools-KYKGATNX.js → devtools-K7FXBBFZ.js} +3 -3
  123. package/dist/error-handler-WK3AZ7A2.js +38 -0
  124. package/dist/hooks/postrun.js +22 -22
  125. package/dist/hooks/prerun.js +22 -22
  126. package/dist/{http-proxy-GGTVQ6CU.js → http-proxy-FXWKYHZ3.js} +14 -24
  127. package/dist/index.js +7229 -7321
  128. package/dist/lib-JVEIEXYB.js +6 -0
  129. package/dist/lib-QZGSY5YB.js +13 -0
  130. package/dist/{local-YCARFR7T.js → local-3ERK45M5.js} +15 -15
  131. package/dist/{magic-string.es-3RXPUXZF.js → magic-string.es-PJMYOE6F.js} +49 -96
  132. package/dist/{morph-SEECJQ2W.js → morph-OSHCID2F.js} +261 -347
  133. package/dist/{multipart-parser-QKUAJJP5.js → multipart-parser-WSNBP656.js} +8 -7
  134. package/dist/{node-NSRIJ2KL.js → node-2KLEBSMO.js} +31 -31
  135. package/dist/{node-package-manager-TLCUDNS2.js → node-package-manager-QIM24GB3.js} +19 -17
  136. package/dist/{npa-RLWNZ3BM.js → npa-TM76BGG3.js} +10 -16
  137. package/dist/{open-MZGVNFZO.js → open-BHIF7E3E.js} +3 -3
  138. package/dist/out-JR4DWQ2G.js +7 -0
  139. package/dist/{path-NDLRQPLA.js → path-2HZUSAGR.js} +3 -3
  140. package/dist/{source-map-7AAPWPHZ.js → source-map-QCVC46UY.js} +2 -2
  141. package/dist/tsconfig.tsbuildinfo +1 -1
  142. package/dist/{ui-5AEI3FU6.js → ui-5AHG256I.js} +15 -15
  143. package/dist/{workerd-SW7QDYB6.js → workerd-MSNALKI2.js} +30 -30
  144. package/oclif.manifest.json +81 -1
  145. package/package.json +8 -8
  146. package/dist/error-handler-MOT24ECB.js +0 -38
  147. package/dist/lib-PPXZBVZX.js +0 -6
  148. package/dist/lib-XYCLX35G.js +0 -13
  149. package/dist/out-MHEKZJWS.js +0 -7
@@ -1,5 +1,819 @@
1
1
  # skeleton
2
2
 
3
+ ## 2024.10.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Remove initial redirect from product display page ([#2643](https://github.com/Shopify/hydrogen/pull/2643)) by [@scottdixon](https://github.com/scottdixon)
8
+
9
+ - Optional updates for the product route and product form to handle combined listing and 2000 variant limit. ([#2659](https://github.com/Shopify/hydrogen/pull/2659)) by [@wizardlyhel](https://github.com/wizardlyhel)
10
+
11
+ 1. Update your SFAPI product query to bring in the new query fields:
12
+
13
+ ```diff
14
+ const PRODUCT_FRAGMENT = `#graphql
15
+ fragment Product on Product {
16
+ id
17
+ title
18
+ vendor
19
+ handle
20
+ descriptionHtml
21
+ description
22
+ + encodedVariantExistence
23
+ + encodedVariantAvailability
24
+ options {
25
+ name
26
+ optionValues {
27
+ name
28
+ + firstSelectableVariant {
29
+ + ...ProductVariant
30
+ + }
31
+ + swatch {
32
+ + color
33
+ + image {
34
+ + previewImage {
35
+ + url
36
+ + }
37
+ + }
38
+ + }
39
+ }
40
+ }
41
+ - selectedVariant: selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {
42
+ + selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {
43
+ + ...ProductVariant
44
+ + }
45
+ + adjacentVariants (selectedOptions: $selectedOptions) {
46
+ + ...ProductVariant
47
+ + }
48
+ - variants(first: 1) {
49
+ - nodes {
50
+ - ...ProductVariant
51
+ - }
52
+ - }
53
+ seo {
54
+ description
55
+ title
56
+ }
57
+ }
58
+ ${PRODUCT_VARIANT_FRAGMENT}
59
+ ` as const;
60
+ ```
61
+
62
+ 2. Update `loadDeferredData` function. We no longer need to load in all the variants. You can also remove `VARIANTS_QUERY` variable.
63
+
64
+ ```diff
65
+ function loadDeferredData({context, params}: LoaderFunctionArgs) {
66
+ + // Put any API calls that is not critical to be available on first page render
67
+ + // For example: product reviews, product recommendations, social feeds.
68
+ - // In order to show which variants are available in the UI, we need to query
69
+ - // all of them. But there might be a *lot*, so instead separate the variants
70
+ - // into it's own separate query that is deferred. So there's a brief moment
71
+ - // where variant options might show as available when they're not, but after
72
+ - // this deferred query resolves, the UI will update.
73
+ - const variants = context.storefront
74
+ - .query(VARIANTS_QUERY, {
75
+ - variables: {handle: params.handle!},
76
+ - })
77
+ - .catch((error) => {
78
+ - // Log query errors, but don't throw them so the page can still render
79
+ - console.error(error);
80
+ - return null;
81
+ - });
82
+
83
+ + return {}
84
+ - return {
85
+ - variants,
86
+ - };
87
+ }
88
+ ```
89
+
90
+ 3. Remove the redirect logic in the `loadCriticalData` function and completely remove `redirectToFirstVariant` function
91
+
92
+ ```diff
93
+ async function loadCriticalData({
94
+ context,
95
+ params,
96
+ request,
97
+ }: LoaderFunctionArgs) {
98
+ const {handle} = params;
99
+ const {storefront} = context;
100
+ if (!handle) {
101
+ throw new Error('Expected product handle to be defined');
102
+ }
103
+ const [{product}] = await Promise.all([
104
+ storefront.query(PRODUCT_QUERY, {
105
+ variables: {handle, selectedOptions: getSelectedProductOptions(request)},
106
+ }),
107
+ // Add other queries here, so that they are loaded in parallel
108
+ ]);
109
+
110
+ if (!product?.id) {
111
+ throw new Response(null, {status: 404});
112
+ }
113
+
114
+ - const firstVariant = product.variants.nodes[0];
115
+ - const firstVariantIsDefault = Boolean(
116
+ - firstVariant.selectedOptions.find(
117
+ - (option: SelectedOption) =>
118
+ - option.name === 'Title' && option.value === 'Default Title',
119
+ - ),
120
+ - );
121
+
122
+ - if (firstVariantIsDefault) {
123
+ - product.selectedVariant = firstVariant;
124
+ - } else {
125
+ - // if no selected variant was returned from the selected options,
126
+ - // we redirect to the first variant's url with it's selected options applied
127
+ - if (!product.selectedVariant) {
128
+ - throw redirectToFirstVariant({product, request});
129
+ - }
130
+ - }
131
+
132
+ return {
133
+ product,
134
+ };
135
+ }
136
+
137
+ ...
138
+
139
+ - function redirectToFirstVariant({
140
+ - product,
141
+ - request,
142
+ - }: {
143
+ - product: ProductFragment;
144
+ - request: Request;
145
+ - }) {
146
+ - ...
147
+ - }
148
+ ```
149
+
150
+ 4. Update the `Product` component to use the new data fields.
151
+
152
+ ```diff
153
+ import {
154
+ getSelectedProductOptions,
155
+ Analytics,
156
+ useOptimisticVariant,
157
+ + getAdjacentAndFirstAvailableVariants,
158
+ } from '@shopify/hydrogen';
159
+
160
+ export default function Product() {
161
+ + const {product} = useLoaderData<typeof loader>();
162
+ - const {product, variants} = useLoaderData<typeof loader>();
163
+
164
+ + // Optimistically selects a variant with given available variant information
165
+ + const selectedVariant = useOptimisticVariant(
166
+ + product.selectedOrFirstAvailableVariant,
167
+ + getAdjacentAndFirstAvailableVariants(product),
168
+ + );
169
+ - const selectedVariant = useOptimisticVariant(
170
+ - product.selectedVariant,
171
+ - variants,
172
+ - );
173
+ ```
174
+
175
+ 5. Handle missing search query param in url from selecting a first variant
176
+
177
+ ```diff
178
+ import {
179
+ getSelectedProductOptions,
180
+ Analytics,
181
+ useOptimisticVariant,
182
+ getAdjacentAndFirstAvailableVariants,
183
+ + useSelectedOptionInUrlParam,
184
+ } from '@shopify/hydrogen';
185
+
186
+ export default function Product() {
187
+ const {product} = useLoaderData<typeof loader>();
188
+
189
+ // Optimistically selects a variant with given available variant information
190
+ const selectedVariant = useOptimisticVariant(
191
+ product.selectedOrFirstAvailableVariant,
192
+ getAdjacentAndFirstAvailableVariants(product),
193
+ );
194
+
195
+ + // Sets the search param to the selected variant without navigation
196
+ + // only when no search params are set in the url
197
+ + useSelectedOptionInUrlParam(selectedVariant.selectedOptions);
198
+ ```
199
+
200
+ 6. Get the product options array using `getProductOptions`
201
+
202
+ ```diff
203
+ import {
204
+ getSelectedProductOptions,
205
+ Analytics,
206
+ useOptimisticVariant,
207
+ + getProductOptions,
208
+ getAdjacentAndFirstAvailableVariants,
209
+ useSelectedOptionInUrlParam,
210
+ } from '@shopify/hydrogen';
211
+
212
+ export default function Product() {
213
+ const {product} = useLoaderData<typeof loader>();
214
+
215
+ // Optimistically selects a variant with given available variant information
216
+ const selectedVariant = useOptimisticVariant(
217
+ product.selectedOrFirstAvailableVariant,
218
+ getAdjacentAndFirstAvailableVariants(product),
219
+ );
220
+
221
+ // Sets the search param to the selected variant without navigation
222
+ // only when no search params are set in the url
223
+ useSelectedOptionInUrlParam(selectedVariant.selectedOptions);
224
+
225
+ + // Get the product options array
226
+ + const productOptions = getProductOptions({
227
+ + ...product,
228
+ + selectedOrFirstAvailableVariant: selectedVariant,
229
+ + });
230
+ ```
231
+
232
+ 7. Remove the `Await` and `Suspense` from the `ProductForm`. We no longer have any queries that we need to wait for.
233
+
234
+ ```diff
235
+ export default function Product() {
236
+
237
+ ...
238
+
239
+ return (
240
+ ...
241
+ + <ProductForm
242
+ + productOptions={productOptions}
243
+ + selectedVariant={selectedVariant}
244
+ + />
245
+ - <Suspense
246
+ - fallback={
247
+ - <ProductForm
248
+ - product={product}
249
+ - selectedVariant={selectedVariant}
250
+ - variants={[]}
251
+ - />
252
+ - }
253
+ - >
254
+ - <Await
255
+ - errorElement="There was a problem loading product variants"
256
+ - resolve={variants}
257
+ - >
258
+ - {(data) => (
259
+ - <ProductForm
260
+ - product={product}
261
+ - selectedVariant={selectedVariant}
262
+ - variants={data?.product?.variants.nodes || []}
263
+ - />
264
+ - )}
265
+ - </Await>
266
+ - </Suspense>
267
+ ```
268
+
269
+ 8. Update the `ProductForm` component.
270
+
271
+ ```tsx
272
+ import {Link, useNavigate} from '@remix-run/react';
273
+ import {type MappedProductOptions} from '@shopify/hydrogen';
274
+ import type {
275
+ Maybe,
276
+ ProductOptionValueSwatch,
277
+ } from '@shopify/hydrogen/storefront-api-types';
278
+ import {AddToCartButton} from './AddToCartButton';
279
+ import {useAside} from './Aside';
280
+ import type {ProductFragment} from 'storefrontapi.generated';
281
+
282
+ export function ProductForm({
283
+ productOptions,
284
+ selectedVariant,
285
+ }: {
286
+ productOptions: MappedProductOptions[];
287
+ selectedVariant: ProductFragment['selectedOrFirstAvailableVariant'];
288
+ }) {
289
+ const navigate = useNavigate();
290
+ const {open} = useAside();
291
+ return (
292
+ <div className="product-form">
293
+ {productOptions.map((option) => (
294
+ <div className="product-options" key={option.name}>
295
+ <h5>{option.name}</h5>
296
+ <div className="product-options-grid">
297
+ {option.optionValues.map((value) => {
298
+ const {
299
+ name,
300
+ handle,
301
+ variantUriQuery,
302
+ selected,
303
+ available,
304
+ exists,
305
+ isDifferentProduct,
306
+ swatch,
307
+ } = value;
308
+
309
+ if (isDifferentProduct) {
310
+ // SEO
311
+ // When the variant is a combined listing child product
312
+ // that leads to a different url, we need to render it
313
+ // as an anchor tag
314
+ return (
315
+ <Link
316
+ className="product-options-item"
317
+ key={option.name + name}
318
+ prefetch="intent"
319
+ preventScrollReset
320
+ replace
321
+ to={`/products/${handle}?${variantUriQuery}`}
322
+ style={{
323
+ border: selected
324
+ ? '1px solid black'
325
+ : '1px solid transparent',
326
+ opacity: available ? 1 : 0.3,
327
+ }}
328
+ >
329
+ <ProductOptionSwatch swatch={swatch} name={name} />
330
+ </Link>
331
+ );
332
+ } else {
333
+ // SEO
334
+ // When the variant is an update to the search param,
335
+ // render it as a button with javascript navigating to
336
+ // the variant so that SEO bots do not index these as
337
+ // duplicated links
338
+ return (
339
+ <button
340
+ type="button"
341
+ className={`product-options-item${
342
+ exists && !selected ? ' link' : ''
343
+ }`}
344
+ key={option.name + name}
345
+ style={{
346
+ border: selected
347
+ ? '1px solid black'
348
+ : '1px solid transparent',
349
+ opacity: available ? 1 : 0.3,
350
+ }}
351
+ disabled={!exists}
352
+ onClick={() => {
353
+ if (!selected) {
354
+ navigate(`?${variantUriQuery}`, {
355
+ replace: true,
356
+ });
357
+ }
358
+ }}
359
+ >
360
+ <ProductOptionSwatch swatch={swatch} name={name} />
361
+ </button>
362
+ );
363
+ }
364
+ })}
365
+ </div>
366
+ <br />
367
+ </div>
368
+ ))}
369
+ <AddToCartButton
370
+ disabled={!selectedVariant || !selectedVariant.availableForSale}
371
+ onClick={() => {
372
+ open('cart');
373
+ }}
374
+ lines={
375
+ selectedVariant
376
+ ? [
377
+ {
378
+ merchandiseId: selectedVariant.id,
379
+ quantity: 1,
380
+ selectedVariant,
381
+ },
382
+ ]
383
+ : []
384
+ }
385
+ >
386
+ {selectedVariant?.availableForSale ? 'Add to cart' : 'Sold out'}
387
+ </AddToCartButton>
388
+ </div>
389
+ );
390
+ }
391
+
392
+ function ProductOptionSwatch({
393
+ swatch,
394
+ name,
395
+ }: {
396
+ swatch?: Maybe<ProductOptionValueSwatch> | undefined;
397
+ name: string;
398
+ }) {
399
+ const image = swatch?.image?.previewImage?.url;
400
+ const color = swatch?.color;
401
+
402
+ if (!image && !color) return name;
403
+
404
+ return (
405
+ <div
406
+ aria-label={name}
407
+ className="product-option-label-swatch"
408
+ style={{
409
+ backgroundColor: color || 'transparent',
410
+ }}
411
+ >
412
+ {!!image && <img src={image} alt={name} />}
413
+ </div>
414
+ );
415
+ }
416
+ ```
417
+
418
+ 9. Update `app.css`
419
+
420
+ ```diff
421
+ + /*
422
+ + * --------------------------------------------------
423
+ + * Non anchor links
424
+ + * --------------------------------------------------
425
+ + */
426
+ + .link:hover {
427
+ + text-decoration: underline;
428
+ + cursor: pointer;
429
+ + }
430
+
431
+ ...
432
+
433
+ - .product-options-item {
434
+ + .product-options-item,
435
+ + .product-options-item:disabled {
436
+ + padding: 0.25rem 0.5rem;
437
+ + background-color: transparent;
438
+ + font-size: 1rem;
439
+ + font-family: inherit;
440
+ + }
441
+
442
+ + .product-option-label-swatch {
443
+ + width: 1.25rem;
444
+ + height: 1.25rem;
445
+ + margin: 0.25rem 0;
446
+ + }
447
+
448
+ + .product-option-label-swatch img {
449
+ + width: 100%;
450
+ + }
451
+ ```
452
+
453
+ 10. Update `lib/variants.ts`
454
+
455
+ Make `useVariantUrl` and `getVariantUrl` flexible to supplying a selected option param
456
+
457
+ ```diff
458
+ export function useVariantUrl(
459
+ handle: string,
460
+ - selectedOptions: SelectedOption[],
461
+ + selectedOptions?: SelectedOption[],
462
+ ) {
463
+ const {pathname} = useLocation();
464
+
465
+ return useMemo(() => {
466
+ return getVariantUrl({
467
+ handle,
468
+ pathname,
469
+ searchParams: new URLSearchParams(),
470
+ selectedOptions,
471
+ });
472
+ }, [handle, selectedOptions, pathname]);
473
+ }
474
+ export function getVariantUrl({
475
+ handle,
476
+ pathname,
477
+ searchParams,
478
+ selectedOptions,
479
+ }: {
480
+ handle: string;
481
+ pathname: string;
482
+ searchParams: URLSearchParams;
483
+ - selectedOptions: SelectedOption[];
484
+ + selectedOptions?: SelectedOption[],
485
+ }) {
486
+ const match = /(\/[a-zA-Z]{2}-[a-zA-Z]{2}\/)/g.exec(pathname);
487
+ const isLocalePathname = match && match.length > 0;
488
+ const path = isLocalePathname
489
+ ? `${match![0]}products/${handle}`
490
+ : `/products/${handle}`;
491
+
492
+ - selectedOptions.forEach((option) => {
493
+ + selectedOptions?.forEach((option) => {
494
+ searchParams.set(option.name, option.value);
495
+ });
496
+ ```
497
+
498
+ 11. Update `routes/collections.$handle.tsx`
499
+
500
+ We no longer need to query for the variants since product route can efficiently
501
+ obtain the first available variants. Update the code to reflect that:
502
+
503
+ ```diff
504
+ const PRODUCT_ITEM_FRAGMENT = `#graphql
505
+ fragment MoneyProductItem on MoneyV2 {
506
+ amount
507
+ currencyCode
508
+ }
509
+ fragment ProductItem on Product {
510
+ id
511
+ handle
512
+ title
513
+ featuredImage {
514
+ id
515
+ altText
516
+ url
517
+ width
518
+ height
519
+ }
520
+ priceRange {
521
+ minVariantPrice {
522
+ ...MoneyProductItem
523
+ }
524
+ maxVariantPrice {
525
+ ...MoneyProductItem
526
+ }
527
+ }
528
+ - variants(first: 1) {
529
+ - nodes {
530
+ - selectedOptions {
531
+ - name
532
+ - value
533
+ - }
534
+ - }
535
+ - }
536
+ }
537
+ ` as const;
538
+ ```
539
+
540
+ and remove the variant reference
541
+
542
+ ```diff
543
+ function ProductItem({
544
+ product,
545
+ loading,
546
+ }: {
547
+ product: ProductItemFragment;
548
+ loading?: 'eager' | 'lazy';
549
+ }) {
550
+ - const variant = product.variants.nodes[0];
551
+ - const variantUrl = useVariantUrl(product.handle, variant.selectedOptions);
552
+ + const variantUrl = useVariantUrl(product.handle);
553
+ return (
554
+ ```
555
+
556
+ 12. Update `routes/collections.all.tsx`
557
+
558
+ Same reasoning as `collections.$handle.tsx`
559
+
560
+ ```diff
561
+ const PRODUCT_ITEM_FRAGMENT = `#graphql
562
+ fragment MoneyProductItem on MoneyV2 {
563
+ amount
564
+ currencyCode
565
+ }
566
+ fragment ProductItem on Product {
567
+ id
568
+ handle
569
+ title
570
+ featuredImage {
571
+ id
572
+ altText
573
+ url
574
+ width
575
+ height
576
+ }
577
+ priceRange {
578
+ minVariantPrice {
579
+ ...MoneyProductItem
580
+ }
581
+ maxVariantPrice {
582
+ ...MoneyProductItem
583
+ }
584
+ }
585
+ - variants(first: 1) {
586
+ - nodes {
587
+ - selectedOptions {
588
+ - name
589
+ - value
590
+ - }
591
+ - }
592
+ - }
593
+ }
594
+ ` as const;
595
+ ```
596
+
597
+ and remove the variant reference
598
+
599
+ ```diff
600
+ function ProductItem({
601
+ product,
602
+ loading,
603
+ }: {
604
+ product: ProductItemFragment;
605
+ loading?: 'eager' | 'lazy';
606
+ }) {
607
+ - const variant = product.variants.nodes[0];
608
+ - const variantUrl = useVariantUrl(product.handle, variant.selectedOptions);
609
+ + const variantUrl = useVariantUrl(product.handle);
610
+ return (
611
+ ```
612
+
613
+ 13. Update `routes/search.tsx`
614
+
615
+ Instead of using the first variant, use `selectedOrFirstAvailableVariant`
616
+
617
+ ```diff
618
+ const SEARCH_PRODUCT_FRAGMENT = `#graphql
619
+ fragment SearchProduct on Product {
620
+ __typename
621
+ handle
622
+ id
623
+ publishedAt
624
+ title
625
+ trackingParameters
626
+ vendor
627
+ - variants(first: 1) {
628
+ - nodes {
629
+ + selectedOrFirstAvailableVariant(
630
+ + selectedOptions: []
631
+ + ignoreUnknownOptions: true
632
+ + caseInsensitiveMatch: true
633
+ + ) {
634
+ id
635
+ image {
636
+ url
637
+ altText
638
+ width
639
+ height
640
+ }
641
+ price {
642
+ amount
643
+ currencyCode
644
+ }
645
+ compareAtPrice {
646
+ amount
647
+ currencyCode
648
+ }
649
+ selectedOptions {
650
+ name
651
+ value
652
+ }
653
+ product {
654
+ handle
655
+ title
656
+ }
657
+ }
658
+ - }
659
+ }
660
+ ` as const;
661
+ ```
662
+
663
+ ```diff
664
+ const PREDICTIVE_SEARCH_PRODUCT_FRAGMENT = `#graphql
665
+ fragment PredictiveProduct on Product {
666
+ __typename
667
+ id
668
+ title
669
+ handle
670
+ trackingParameters
671
+ - variants(first: 1) {
672
+ - nodes {
673
+ + selectedOrFirstAvailableVariant(
674
+ + selectedOptions: []
675
+ + ignoreUnknownOptions: true
676
+ + caseInsensitiveMatch: true
677
+ + ) {
678
+ id
679
+ image {
680
+ url
681
+ altText
682
+ width
683
+ height
684
+ }
685
+ price {
686
+ amount
687
+ currencyCode
688
+ }
689
+ }
690
+ - }
691
+ }
692
+ ```
693
+
694
+ 14. Update `components/SearchResults.tsx`
695
+
696
+ ```diff
697
+ function SearchResultsProducts({
698
+ term,
699
+ products,
700
+ }: PartialSearchResult<'products'>) {
701
+ if (!products?.nodes.length) {
702
+ return null;
703
+ }
704
+
705
+ return (
706
+ <div className="search-result">
707
+ <h2>Products</h2>
708
+ <Pagination connection={products}>
709
+ {({nodes, isLoading, NextLink, PreviousLink}) => {
710
+ const ItemsMarkup = nodes.map((product) => {
711
+ const productUrl = urlWithTrackingParams({
712
+ baseUrl: `/products/${product.handle}`,
713
+ trackingParams: product.trackingParameters,
714
+ term,
715
+ });
716
+
717
+ + const price = product?.selectedOrFirstAvailableVariant?.price;
718
+ + const image = product?.selectedOrFirstAvailableVariant?.image;
719
+
720
+ return (
721
+ <div className="search-results-item" key={product.id}>
722
+ <Link prefetch="intent" to={productUrl}>
723
+ - {product.variants.nodes[0].image && (
724
+ + {image && (
725
+ <Image
726
+ - data={product.variants.nodes[0].image}
727
+ + data={image}
728
+ alt={product.title}
729
+ width={50}
730
+ />
731
+ )}
732
+ <div>
733
+ <p>{product.title}</p>
734
+ <small>
735
+ - <Money data={product.variants.nodes[0].price} />
736
+ + {price &&
737
+ + <Money data={price} />
738
+ + }
739
+ </small>
740
+ </div>
741
+ </Link>
742
+ </div>
743
+ );
744
+ });
745
+ ```
746
+
747
+ 15. Update `components/SearchResultsPredictive.tsx`
748
+
749
+ ```diff
750
+ function SearchResultsPredictiveProducts({
751
+ term,
752
+ products,
753
+ closeSearch,
754
+ }: PartialPredictiveSearchResult<'products'>) {
755
+ if (!products.length) return null;
756
+
757
+ return (
758
+ <div className="predictive-search-result" key="products">
759
+ <h5>Products</h5>
760
+ <ul>
761
+ {products.map((product) => {
762
+ const productUrl = urlWithTrackingParams({
763
+ baseUrl: `/products/${product.handle}`,
764
+ trackingParams: product.trackingParameters,
765
+ term: term.current,
766
+ });
767
+
768
+ + const price = product?.selectedOrFirstAvailableVariant?.price;
769
+ - const image = product?.variants?.nodes?.[0].image;
770
+ + const image = product?.selectedOrFirstAvailableVariant?.image;
771
+ return (
772
+ <li className="predictive-search-result-item" key={product.id}>
773
+ <Link to={productUrl} onClick={closeSearch}>
774
+ {image && (
775
+ <Image
776
+ alt={image.altText ?? ''}
777
+ src={image.url}
778
+ width={50}
779
+ height={50}
780
+ />
781
+ )}
782
+ <div>
783
+ <p>{product.title}</p>
784
+ <small>
785
+ - {product?.variants?.nodes?.[0].price && (
786
+ + {price && (
787
+ - <Money data={product.variants.nodes[0].price} />
788
+ + <Money data={price} />
789
+ )}
790
+ </small>
791
+ </div>
792
+ </Link>
793
+ </li>
794
+ );
795
+ })}
796
+ </ul>
797
+ </div>
798
+ );
799
+ }
800
+ ```
801
+
802
+ - Update `Aside` to have an accessible close button label ([#2639](https://github.com/Shopify/hydrogen/pull/2639)) by [@lb-](https://github.com/lb-)
803
+
804
+ - Fix cart route so that it works with no-js ([#2665](https://github.com/Shopify/hydrogen/pull/2665)) by [@wizardlyhel](https://github.com/wizardlyhel)
805
+
806
+ - Bump Shopify cli version ([#2667](https://github.com/Shopify/hydrogen/pull/2667)) by [@wizardlyhel](https://github.com/wizardlyhel)
807
+
808
+ - Updated dependencies [[`8f64915e`](https://github.com/Shopify/hydrogen/commit/8f64915e934130299307417627a12caf756cd8da), [`a57d5267`](https://github.com/Shopify/hydrogen/commit/a57d5267daa2f22fe1a426fb9f62c242957f95b6), [`91d60fd2`](https://github.com/Shopify/hydrogen/commit/91d60fd2174b7c34f9f6b781cd5f0a70371fd899), [`23a80f3e`](https://github.com/Shopify/hydrogen/commit/23a80f3e7bf9f9908130fc9345397fc694420364)]:
809
+ - @shopify/hydrogen@2024.10.1
810
+
811
+ ## 2024.10.1
812
+
813
+ ### Patch Changes
814
+
815
+ - Bump to get new cli package version by [@wizardlyhel](https://github.com/wizardlyhel)
816
+
3
817
  ## 2024.10.0
4
818
 
5
819
  ### Patch Changes