@shopify/cli 3.72.2 → 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-4HTVLK2H.js → chunk-3OXCI3HX.js} +1402 -1989
  24. package/dist/{chunk-2YCRDHPC.js → chunk-5OFJTURZ.js} +4 -4
  25. package/dist/{chunk-TAKO7LSJ.js → chunk-65GSOZHL.js} +7 -7
  26. package/dist/{chunk-6IUL3QZW.js → chunk-66HLUVKV.js} +4 -4
  27. package/dist/{chunk-N2V4IFX3.js → chunk-6PEBJVBW.js} +3 -3
  28. package/dist/{chunk-XVNW332R.js → chunk-75LV6AQS.js} +6 -10
  29. package/dist/{chunk-QXIIXX7G.js → chunk-7RKRCUFA.js} +16518 -24140
  30. package/dist/{chunk-ZESD7DR7.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-YXJHYEP7.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-PQQTBTYM.js → chunk-ELTOJBOJ.js} +4 -5
  37. package/dist/{chunk-NUP5TATA.js → chunk-EZQWZ57B.js} +2 -2
  38. package/dist/{chunk-YKUYPSCY.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-RCRRAFH7.js → chunk-GDLROW57.js} +3 -3
  42. package/dist/{chunk-KMZ5JSGS.js → chunk-I25PGLBO.js} +3 -3
  43. package/dist/{chunk-OXKHBIW7.js → chunk-IG5SOACB.js} +29 -34
  44. package/dist/{chunk-UIRMJZRW.js → chunk-IRHYYIN7.js} +4 -4
  45. package/dist/{chunk-OWLPHMUA.js → chunk-J673ZU5S.js} +4 -4
  46. package/dist/{chunk-HYGCZ6GV.js → chunk-JORKLY7M.js} +68 -49
  47. package/dist/{chunk-K6Y4FYT5.js → chunk-K7HGDAI4.js} +17 -20
  48. package/dist/{chunk-I6R52HNI.js → chunk-KF2D6QHZ.js} +39 -68
  49. package/dist/{chunk-KGKTCQ7H.js → chunk-KMWARALD.js} +5 -5
  50. package/dist/{chunk-VWNWE4HG.js → chunk-KTNFE44J.js} +110 -83
  51. package/dist/{chunk-BYPQXSAL.js → chunk-KUM3DVPF.js} +17 -13
  52. package/dist/{chunk-K7B4JJLF.js → chunk-KZBL6BQ7.js} +4 -4
  53. package/dist/{chunk-WNDN5FAY.js → chunk-MHUINF7I.js} +3 -3
  54. package/dist/{chunk-L2J5VM7R.js → chunk-NFQLKURK.js} +41 -68
  55. package/dist/{chunk-KCI6QCAV.js → chunk-O77L7CCL.js} +4 -4
  56. package/dist/{chunk-SNOECVP4.js → chunk-OAZFIMJ3.js} +2 -2
  57. package/dist/{chunk-GVNIHXMX.js → chunk-OE3IXTC5.js} +38 -60
  58. package/dist/{chunk-ECWFBV2F.js → chunk-ONOLOXLM.js} +5 -5
  59. package/dist/{chunk-POZ5MGPT.js → chunk-PKR7KJ6P.js} +2 -3
  60. package/dist/{chunk-JDM5VOXB.js → chunk-PRWEHR2C.js} +3 -3
  61. package/dist/{chunk-SO2RZ6TZ.js → chunk-QNK2EAZ3.js} +5 -5
  62. package/dist/{chunk-E37RRDIH.js → chunk-RCA7PFH4.js} +4 -4
  63. package/dist/{chunk-GKEFW755.js → chunk-RK7JAMCI.js} +6 -6
  64. package/dist/{chunk-HMUOOT55.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-NE43V3EI.js → chunk-SUUVDRTQ.js} +6 -7
  68. package/dist/{chunk-VJ7TIVX7.js → chunk-TEHNKBLD.js} +6 -7
  69. package/dist/{chunk-KVEY52WG.js → chunk-VPNXQGG6.js} +4 -4
  70. package/dist/{chunk-NXHZX3WR.js → chunk-X5FJXK25.js} +9 -9
  71. package/dist/{chunk-PMUQTGZJ.js → chunk-X7YTIMNN.js} +4 -6
  72. package/dist/{chunk-UIAIRQSP.js → chunk-XAGT2UNE.js} +3 -3
  73. package/dist/{chunk-7Q3MMWAC.js → chunk-XE5EOEBL.js} +2 -2
  74. package/dist/{chunk-PKJLXLTR.js → chunk-YP7WU5EU.js} +5 -5
  75. package/dist/{chunk-QDPQB6WU.js → chunk-YPTEMDFR.js} +5 -5
  76. package/dist/{chunk-KWLJTNRE.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-G2DAJL7B.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 +7228 -7320
  128. package/dist/lib-JVEIEXYB.js +6 -0
  129. package/dist/lib-QZGSY5YB.js +13 -0
  130. package/dist/{local-Q5ZG2NHX.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-S6CM3NTX.js → node-2KLEBSMO.js} +31 -31
  135. package/dist/{node-package-manager-GUY5IO3W.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-AWSBLSD4.js → ui-5AHG256I.js} +15 -15
  143. package/dist/{workerd-3HGLHQET.js → workerd-MSNALKI2.js} +30 -30
  144. package/oclif.manifest.json +81 -1
  145. package/package.json +8 -8
  146. package/dist/error-handler-6HCFKLTC.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
@@ -62,7 +62,7 @@ export function Aside({
62
62
  <aside>
63
63
  <header>
64
64
  <h3>{heading}</h3>
65
- <button className="close reset" onClick={close}>
65
+ <button className="close reset" onClick={close} aria-label="Close">
66
66
  &times;
67
67
  </button>
68
68
  </header>
@@ -4,7 +4,6 @@ import {Pagination} from '@shopify/hydrogen';
4
4
  /**
5
5
  * <PaginatedResourceSection > is a component that encapsulate how the previous and next behaviors throughout your application.
6
6
  */
7
-
8
7
  export function PaginatedResourceSection<NodesType>({
9
8
  connection,
10
9
  children,
@@ -17,7 +16,7 @@ export function PaginatedResourceSection<NodesType>({
17
16
  return (
18
17
  <Pagination connection={connection}>
19
18
  {({nodes, isLoading, PreviousLink, NextLink}) => {
20
- const resoucesMarkup = nodes.map((node, index) =>
19
+ const resourcesMarkup = nodes.map((node, index) =>
21
20
  children({node, index}),
22
21
  );
23
22
 
@@ -27,9 +26,9 @@ export function PaginatedResourceSection<NodesType>({
27
26
  {isLoading ? 'Loading...' : <span>↑ Load previous</span>}
28
27
  </PreviousLink>
29
28
  {resourcesClassName ? (
30
- <div className={resourcesClassName}>{resoucesMarkup}</div>
29
+ <div className={resourcesClassName}>{resourcesMarkup}</div>
31
30
  ) : (
32
- resoucesMarkup
31
+ resourcesMarkup
33
32
  )}
34
33
  <NextLink>
35
34
  {isLoading ? 'Loading...' : <span>Load more ↓</span>}
@@ -1,32 +1,105 @@
1
- import {Link} from '@remix-run/react';
2
- import {type VariantOption, VariantSelector} from '@shopify/hydrogen';
1
+ import {Link, useNavigate} from '@remix-run/react';
2
+ import {type MappedProductOptions} from '@shopify/hydrogen';
3
3
  import type {
4
- ProductFragment,
5
- ProductVariantFragment,
6
- } from 'storefrontapi.generated';
7
- import {AddToCartButton} from '~/components/AddToCartButton';
8
- import {useAside} from '~/components/Aside';
4
+ Maybe,
5
+ ProductOptionValueSwatch,
6
+ } from '@shopify/hydrogen/storefront-api-types';
7
+ import {AddToCartButton} from './AddToCartButton';
8
+ import {useAside} from './Aside';
9
+ import type {ProductFragment} from 'storefrontapi.generated';
9
10
 
10
11
  export function ProductForm({
11
- product,
12
+ productOptions,
12
13
  selectedVariant,
13
- variants,
14
14
  }: {
15
- product: ProductFragment;
16
- selectedVariant: ProductFragment['selectedVariant'];
17
- variants: Array<ProductVariantFragment>;
15
+ productOptions: MappedProductOptions[];
16
+ selectedVariant: ProductFragment['selectedOrFirstAvailableVariant'];
18
17
  }) {
18
+ const navigate = useNavigate();
19
19
  const {open} = useAside();
20
20
  return (
21
21
  <div className="product-form">
22
- <VariantSelector
23
- handle={product.handle}
24
- options={product.options.filter((option) => option.optionValues.length > 1)}
25
- variants={variants}
26
- >
27
- {({option}) => <ProductOptions key={option.name} option={option} />}
28
- </VariantSelector>
29
- <br />
22
+ {productOptions.map((option) => {
23
+ // If there is only a single value in the option values, don't display the option
24
+ if (option.optionValues.length === 1) return null;
25
+
26
+ return (
27
+ <div className="product-options" key={option.name}>
28
+ <h5>{option.name}</h5>
29
+ <div className="product-options-grid">
30
+ {option.optionValues.map((value) => {
31
+ const {
32
+ name,
33
+ handle,
34
+ variantUriQuery,
35
+ selected,
36
+ available,
37
+ exists,
38
+ isDifferentProduct,
39
+ swatch,
40
+ } = value;
41
+
42
+ if (isDifferentProduct) {
43
+ // SEO
44
+ // When the variant is a combined listing child product
45
+ // that leads to a different url, we need to render it
46
+ // as an anchor tag
47
+ return (
48
+ <Link
49
+ className="product-options-item"
50
+ key={option.name + name}
51
+ prefetch="intent"
52
+ preventScrollReset
53
+ replace
54
+ to={`/products/${handle}?${variantUriQuery}`}
55
+ style={{
56
+ border: selected
57
+ ? '1px solid black'
58
+ : '1px solid transparent',
59
+ opacity: available ? 1 : 0.3,
60
+ }}
61
+ >
62
+ <ProductOptionSwatch swatch={swatch} name={name} />
63
+ </Link>
64
+ );
65
+ } else {
66
+ // SEO
67
+ // When the variant is an update to the search param,
68
+ // render it as a button with javascript navigating to
69
+ // the variant so that SEO bots do not index these as
70
+ // duplicated links
71
+ return (
72
+ <button
73
+ type="button"
74
+ className={`product-options-item${
75
+ exists && !selected ? ' link' : ''
76
+ }`}
77
+ key={option.name + name}
78
+ style={{
79
+ border: selected
80
+ ? '1px solid black'
81
+ : '1px solid transparent',
82
+ opacity: available ? 1 : 0.3,
83
+ }}
84
+ disabled={!exists}
85
+ onClick={() => {
86
+ if (!selected) {
87
+ navigate(`?${variantUriQuery}`, {
88
+ replace: true,
89
+ });
90
+ }
91
+ }}
92
+ >
93
+ <ProductOptionSwatch swatch={swatch} name={name} />
94
+ </button>
95
+ );
96
+ }
97
+ })}
98
+ </div>
99
+ <br />
100
+ </div>
101
+ )
102
+ })}
30
103
  <AddToCartButton
31
104
  disabled={!selectedVariant || !selectedVariant.availableForSale}
32
105
  onClick={() => {
@@ -50,31 +123,27 @@ export function ProductForm({
50
123
  );
51
124
  }
52
125
 
53
- function ProductOptions({option}: {option: VariantOption}) {
126
+ function ProductOptionSwatch({
127
+ swatch,
128
+ name,
129
+ }: {
130
+ swatch?: Maybe<ProductOptionValueSwatch> | undefined;
131
+ name: string;
132
+ }) {
133
+ const image = swatch?.image?.previewImage?.url;
134
+ const color = swatch?.color;
135
+
136
+ if (!image && !color) return name;
137
+
54
138
  return (
55
- <div className="product-options" key={option.name}>
56
- <h5>{option.name}</h5>
57
- <div className="product-options-grid">
58
- {option.values.map(({value, isAvailable, isActive, to}) => {
59
- return (
60
- <Link
61
- className="product-options-item"
62
- key={option.name + value}
63
- prefetch="intent"
64
- preventScrollReset
65
- replace
66
- to={to}
67
- style={{
68
- border: isActive ? '1px solid black' : '1px solid transparent',
69
- opacity: isAvailable ? 1 : 0.3,
70
- }}
71
- >
72
- {value}
73
- </Link>
74
- );
75
- })}
76
- </div>
77
- <br />
139
+ <div
140
+ aria-label={name}
141
+ className="product-option-label-swatch"
142
+ style={{
143
+ backgroundColor: color || 'transparent',
144
+ }}
145
+ >
146
+ {!!image && <img src={image} alt={name} />}
78
147
  </div>
79
148
  );
80
149
  }
@@ -113,12 +113,15 @@ function SearchResultsProducts({
113
113
  term,
114
114
  });
115
115
 
116
+ const price = product?.selectedOrFirstAvailableVariant?.price;
117
+ const image = product?.selectedOrFirstAvailableVariant?.image;
118
+
116
119
  return (
117
120
  <div className="search-results-item" key={product.id}>
118
121
  <Link prefetch="intent" to={productUrl}>
119
- {product.variants.nodes[0].image && (
122
+ {image && (
120
123
  <Image
121
- data={product.variants.nodes[0].image}
124
+ data={image}
122
125
  alt={product.title}
123
126
  width={50}
124
127
  />
@@ -126,7 +129,9 @@ function SearchResultsProducts({
126
129
  <div>
127
130
  <p>{product.title}</p>
128
131
  <small>
129
- <Money data={product.variants.nodes[0].price} />
132
+ {price &&
133
+ <Money data={price} />
134
+ }
130
135
  </small>
131
136
  </div>
132
137
  </Link>
@@ -133,7 +133,7 @@ function SearchResultsPredictiveCollections({
133
133
  <h5>Collections</h5>
134
134
  <ul>
135
135
  {collections.map((collection) => {
136
- const colllectionUrl = urlWithTrackingParams({
136
+ const collectionUrl = urlWithTrackingParams({
137
137
  baseUrl: `/collections/${collection.handle}`,
138
138
  trackingParams: collection.trackingParameters,
139
139
  term: term.current,
@@ -141,7 +141,7 @@ function SearchResultsPredictiveCollections({
141
141
 
142
142
  return (
143
143
  <li className="predictive-search-result-item" key={collection.id}>
144
- <Link onClick={closeSearch} to={colllectionUrl}>
144
+ <Link onClick={closeSearch} to={collectionUrl}>
145
145
  {collection.image?.url && (
146
146
  <Image
147
147
  alt={collection.image.altText ?? ''}
@@ -213,7 +213,8 @@ function SearchResultsPredictiveProducts({
213
213
  term: term.current,
214
214
  });
215
215
 
216
- const image = product?.variants?.nodes?.[0].image;
216
+ const price = product?.selectedOrFirstAvailableVariant?.price;
217
+ const image = product?.selectedOrFirstAvailableVariant?.image;
217
218
  return (
218
219
  <li className="predictive-search-result-item" key={product.id}>
219
220
  <Link to={productUrl} onClick={closeSearch}>
@@ -228,8 +229,8 @@ function SearchResultsPredictiveProducts({
228
229
  <div>
229
230
  <p>{product.title}</p>
230
231
  <small>
231
- {product?.variants?.nodes?.[0].price && (
232
- <Money data={product.variants.nodes[0].price} />
232
+ {price && (
233
+ <Money data={price} />
233
234
  )}
234
235
  </small>
235
236
  </div>
@@ -4,7 +4,7 @@ import {useMemo} from 'react';
4
4
 
5
5
  export function useVariantUrl(
6
6
  handle: string,
7
- selectedOptions: SelectedOption[],
7
+ selectedOptions?: SelectedOption[],
8
8
  ) {
9
9
  const {pathname} = useLocation();
10
10
 
@@ -27,7 +27,7 @@ export function getVariantUrl({
27
27
  handle: string;
28
28
  pathname: string;
29
29
  searchParams: URLSearchParams;
30
- selectedOptions: SelectedOption[];
30
+ selectedOptions?: SelectedOption[];
31
31
  }) {
32
32
  const match = /(\/[a-zA-Z]{2}-[a-zA-Z]{2}\/)/g.exec(pathname);
33
33
  const isLocalePathname = match && match.length > 0;
@@ -36,7 +36,7 @@ export function getVariantUrl({
36
36
  ? `${match![0]}products/${handle}`
37
37
  : `/products/${handle}`;
38
38
 
39
- selectedOptions.forEach((option) => {
39
+ selectedOptions?.forEach((option) => {
40
40
  searchParams.set(option.name, option.value);
41
41
  });
42
42
 
@@ -1,6 +1,6 @@
1
1
  import {redirect, type ActionFunctionArgs} from '@shopify/remix-oxygen';
2
2
 
3
- // if we dont implement this, /account/logout will get caught by account.$.tsx to do login
3
+ // if we don't implement this, /account/logout will get caught by account.$.tsx to do login
4
4
  export async function loader() {
5
5
  return redirect('/');
6
6
  }
@@ -1,10 +1,8 @@
1
- import {Await, type MetaFunction, useRouteLoaderData} from '@remix-run/react';
2
- import {Suspense} from 'react';
1
+ import {type MetaFunction, useLoaderData} from '@remix-run/react';
3
2
  import type {CartQueryDataReturn} from '@shopify/hydrogen';
4
3
  import {CartForm} from '@shopify/hydrogen';
5
- import {json, type ActionFunctionArgs} from '@shopify/remix-oxygen';
4
+ import {json, type LoaderFunctionArgs, type ActionFunctionArgs} from '@shopify/remix-oxygen';
6
5
  import {CartMain} from '~/components/CartMain';
7
- import type {RootLoader} from '~/root';
8
6
 
9
7
  export const meta: MetaFunction = () => {
10
8
  return [{title: `Hydrogen | Cart`}];
@@ -95,23 +93,18 @@ export async function action({request, context}: ActionFunctionArgs) {
95
93
  );
96
94
  }
97
95
 
96
+ export async function loader({context}: LoaderFunctionArgs) {
97
+ const {cart} = context;
98
+ return json(await cart.get());
99
+ }
100
+
98
101
  export default function Cart() {
99
- const rootData = useRouteLoaderData<RootLoader>('root');
100
- if (!rootData) return null;
102
+ const cart = useLoaderData<typeof loader>();
101
103
 
102
104
  return (
103
105
  <div className="cart">
104
106
  <h1>Cart</h1>
105
- <Suspense fallback={<p>Loading cart ...</p>}>
106
- <Await
107
- resolve={rootData.cart}
108
- errorElement={<div>An error occurred</div>}
109
- >
110
- {(cart) => {
111
- return <CartMain layout="page" cart={cart} />;
112
- }}
113
- </Await>
114
- </Suspense>
107
+ <CartMain layout="page" cart={cart} />
115
108
  </div>
116
109
  );
117
110
  }
@@ -108,8 +108,7 @@ function ProductItem({
108
108
  product: ProductItemFragment;
109
109
  loading?: 'eager' | 'lazy';
110
110
  }) {
111
- const variant = product.variants.nodes[0];
112
- const variantUrl = useVariantUrl(product.handle, variant.selectedOptions);
111
+ const variantUrl = useVariantUrl(product.handle);
113
112
  return (
114
113
  <Link
115
114
  className="product-item"
@@ -158,14 +157,6 @@ const PRODUCT_ITEM_FRAGMENT = `#graphql
158
157
  ...MoneyProductItem
159
158
  }
160
159
  }
161
- variants(first: 1) {
162
- nodes {
163
- selectedOptions {
164
- name
165
- value
166
- }
167
- }
168
- }
169
160
  }
170
161
  ` as const;
171
162
 
@@ -76,8 +76,7 @@ function ProductItem({
76
76
  product: ProductItemFragment;
77
77
  loading?: 'eager' | 'lazy';
78
78
  }) {
79
- const variant = product.variants.nodes[0];
80
- const variantUrl = useVariantUrl(product.handle, variant.selectedOptions);
79
+ const variantUrl = useVariantUrl(product.handle);
81
80
  return (
82
81
  <Link
83
82
  className="product-item"
@@ -126,14 +125,6 @@ const PRODUCT_ITEM_FRAGMENT = `#graphql
126
125
  ...MoneyProductItem
127
126
  }
128
127
  }
129
- variants(first: 1) {
130
- nodes {
131
- selectedOptions {
132
- name
133
- value
134
- }
135
- }
136
- }
137
128
  }
138
129
  ` as const;
139
130
 
@@ -1,20 +1,25 @@
1
- import {Suspense} from 'react';
2
- import {defer, redirect, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
3
- import {Await, useLoaderData, type MetaFunction} from '@remix-run/react';
4
- import type {ProductFragment} from 'storefrontapi.generated';
1
+ import {defer, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
2
+ import {useLoaderData, type MetaFunction} from '@remix-run/react';
5
3
  import {
6
4
  getSelectedProductOptions,
7
5
  Analytics,
8
6
  useOptimisticVariant,
7
+ getProductOptions,
8
+ getAdjacentAndFirstAvailableVariants,
9
+ useSelectedOptionInUrlParam,
9
10
  } from '@shopify/hydrogen';
10
- import type {SelectedOption} from '@shopify/hydrogen/storefront-api-types';
11
- import {getVariantUrl} from '~/lib/variants';
12
11
  import {ProductPrice} from '~/components/ProductPrice';
13
12
  import {ProductImage} from '~/components/ProductImage';
14
13
  import {ProductForm} from '~/components/ProductForm';
15
14
 
16
15
  export const meta: MetaFunction<typeof loader> = ({data}) => {
17
- return [{title: `Hydrogen | ${data?.product.title ?? ''}`}];
16
+ return [
17
+ {title: `Hydrogen | ${data?.product.title ?? ''}`},
18
+ {
19
+ rel: 'canonical',
20
+ href: `/products/${data?.product.handle}`,
21
+ },
22
+ ];
18
23
  };
19
24
 
20
25
  export async function loader(args: LoaderFunctionArgs) {
@@ -54,24 +59,6 @@ async function loadCriticalData({
54
59
  throw new Response(null, {status: 404});
55
60
  }
56
61
 
57
- const firstVariant = product.variants.nodes[0];
58
- const firstVariantIsDefault = Boolean(
59
- firstVariant.selectedOptions.find(
60
- (option: SelectedOption) =>
61
- option.name === 'Title' && option.value === 'Default Title',
62
- ),
63
- );
64
-
65
- if (firstVariantIsDefault) {
66
- product.selectedVariant = firstVariant;
67
- } else {
68
- // if no selected variant was returned from the selected options,
69
- // we redirect to the first variant's url with it's selected options applied
70
- if (!product.selectedVariant) {
71
- throw redirectToFirstVariant({product, request});
72
- }
73
- }
74
-
75
62
  return {
76
63
  product,
77
64
  };
@@ -83,56 +70,31 @@ async function loadCriticalData({
83
70
  * Make sure to not throw any errors here, as it will cause the page to 500.
84
71
  */
85
72
  function loadDeferredData({context, params}: LoaderFunctionArgs) {
86
- // In order to show which variants are available in the UI, we need to query
87
- // all of them. But there might be a *lot*, so instead separate the variants
88
- // into it's own separate query that is deferred. So there's a brief moment
89
- // where variant options might show as available when they're not, but after
90
- // this deffered query resolves, the UI will update.
91
- const variants = context.storefront
92
- .query(VARIANTS_QUERY, {
93
- variables: {handle: params.handle!},
94
- })
95
- .catch((error) => {
96
- // Log query errors, but don't throw them so the page can still render
97
- console.error(error);
98
- return null;
99
- });
100
-
101
- return {
102
- variants,
103
- };
104
- }
105
-
106
- function redirectToFirstVariant({
107
- product,
108
- request,
109
- }: {
110
- product: ProductFragment;
111
- request: Request;
112
- }) {
113
- const url = new URL(request.url);
114
- const firstVariant = product.variants.nodes[0];
73
+ // Put any API calls that is not critical to be available on first page render
74
+ // For example: product reviews, product recommendations, social feeds.
115
75
 
116
- return redirect(
117
- getVariantUrl({
118
- pathname: url.pathname,
119
- handle: product.handle,
120
- selectedOptions: firstVariant.selectedOptions,
121
- searchParams: new URLSearchParams(url.search),
122
- }),
123
- {
124
- status: 302,
125
- },
126
- );
76
+ return {};
127
77
  }
128
78
 
129
79
  export default function Product() {
130
- const {product, variants} = useLoaderData<typeof loader>();
80
+ const {product} = useLoaderData<typeof loader>();
81
+
82
+ // Optimistically selects a variant with given available variant information
131
83
  const selectedVariant = useOptimisticVariant(
132
- product.selectedVariant,
133
- variants,
84
+ product.selectedOrFirstAvailableVariant,
85
+ getAdjacentAndFirstAvailableVariants(product),
134
86
  );
135
87
 
88
+ // Sets the search param to the selected variant without navigation
89
+ // only when no search params are set in the url
90
+ useSelectedOptionInUrlParam(selectedVariant.selectedOptions);
91
+
92
+ // Get the product options array
93
+ const productOptions = getProductOptions({
94
+ ...product,
95
+ selectedOrFirstAvailableVariant: selectedVariant,
96
+ });
97
+
136
98
  const {title, descriptionHtml} = product;
137
99
 
138
100
  return (
@@ -145,28 +107,10 @@ export default function Product() {
145
107
  compareAtPrice={selectedVariant?.compareAtPrice}
146
108
  />
147
109
  <br />
148
- <Suspense
149
- fallback={
150
- <ProductForm
151
- product={product}
152
- selectedVariant={selectedVariant}
153
- variants={[]}
154
- />
155
- }
156
- >
157
- <Await
158
- errorElement="There was a problem loading product variants"
159
- resolve={variants}
160
- >
161
- {(data) => (
162
- <ProductForm
163
- product={product}
164
- selectedVariant={selectedVariant}
165
- variants={data?.product?.variants.nodes || []}
166
- />
167
- )}
168
- </Await>
169
- </Suspense>
110
+ <ProductForm
111
+ productOptions={productOptions}
112
+ selectedVariant={selectedVariant}
113
+ />
170
114
  <br />
171
115
  <br />
172
116
  <p>
@@ -240,19 +184,30 @@ const PRODUCT_FRAGMENT = `#graphql
240
184
  handle
241
185
  descriptionHtml
242
186
  description
187
+ encodedVariantExistence
188
+ encodedVariantAvailability
243
189
  options {
244
190
  name
245
191
  optionValues {
246
192
  name
193
+ firstSelectableVariant {
194
+ ...ProductVariant
195
+ }
196
+ swatch {
197
+ color
198
+ image {
199
+ previewImage {
200
+ url
201
+ }
202
+ }
203
+ }
247
204
  }
248
205
  }
249
- selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {
206
+ selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {
250
207
  ...ProductVariant
251
208
  }
252
- variants(first: 1) {
253
- nodes {
254
- ...ProductVariant
255
- }
209
+ adjacentVariants (selectedOptions: $selectedOptions) {
210
+ ...ProductVariant
256
211
  }
257
212
  seo {
258
213
  description
@@ -275,27 +230,3 @@ const PRODUCT_QUERY = `#graphql
275
230
  }
276
231
  ${PRODUCT_FRAGMENT}
277
232
  ` as const;
278
-
279
- const PRODUCT_VARIANTS_FRAGMENT = `#graphql
280
- fragment ProductVariants on Product {
281
- variants(first: 250) {
282
- nodes {
283
- ...ProductVariant
284
- }
285
- }
286
- }
287
- ${PRODUCT_VARIANT_FRAGMENT}
288
- ` as const;
289
-
290
- const VARIANTS_QUERY = `#graphql
291
- ${PRODUCT_VARIANTS_FRAGMENT}
292
- query ProductVariants(
293
- $country: CountryCode
294
- $language: LanguageCode
295
- $handle: String!
296
- ) @inContext(country: $country, language: $language) {
297
- product(handle: $handle) {
298
- ...ProductVariants
299
- }
300
- }
301
- ` as const;