@shopify/cli-hydrogen 3.26.0 → 4.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.
Files changed (249) hide show
  1. package/dist/commands/hydrogen/build.js +89 -0
  2. package/dist/commands/hydrogen/dev.js +116 -0
  3. package/dist/commands/hydrogen/init.js +42 -0
  4. package/dist/commands/hydrogen/preview.js +34 -0
  5. package/dist/hooks/init.js +21 -0
  6. package/dist/templates/demo-store/.editorconfig +8 -0
  7. package/dist/templates/demo-store/.eslintignore +4 -0
  8. package/dist/templates/demo-store/.eslintrc.js +16 -0
  9. package/dist/templates/demo-store/.graphqlrc.yml +1 -0
  10. package/dist/templates/demo-store/.prettierignore +2 -0
  11. package/dist/templates/demo-store/.turbo/turbo-build.log +13 -0
  12. package/dist/templates/demo-store/app/components/AccountAddressBook.tsx +97 -0
  13. package/dist/templates/demo-store/app/components/AccountDetails.tsx +41 -0
  14. package/dist/templates/demo-store/app/components/AddToCartButton.tsx +42 -0
  15. package/dist/templates/demo-store/app/components/Breadcrumbs.tsx +36 -0
  16. package/dist/templates/demo-store/app/components/Button.tsx +56 -0
  17. package/dist/templates/demo-store/app/components/Cart.tsx +431 -0
  18. package/dist/templates/demo-store/app/components/CartLoading.tsx +50 -0
  19. package/dist/templates/demo-store/app/components/CountrySelector.tsx +180 -0
  20. package/dist/templates/demo-store/app/components/Drawer.tsx +115 -0
  21. package/dist/templates/demo-store/app/components/FeaturedCollections.tsx +54 -0
  22. package/dist/templates/demo-store/app/components/FeaturedProducts.tsx +116 -0
  23. package/dist/templates/demo-store/app/components/FeaturedSection.tsx +39 -0
  24. package/dist/templates/demo-store/app/components/GenericError.tsx +58 -0
  25. package/dist/templates/demo-store/app/components/Grid.tsx +44 -0
  26. package/dist/templates/demo-store/app/components/Hero.tsx +136 -0
  27. package/dist/templates/demo-store/app/components/Icon.tsx +253 -0
  28. package/dist/templates/demo-store/app/components/Input.tsx +24 -0
  29. package/dist/templates/demo-store/app/components/Layout.tsx +492 -0
  30. package/dist/templates/demo-store/app/components/Link.tsx +46 -0
  31. package/dist/templates/demo-store/app/components/Modal.tsx +46 -0
  32. package/dist/templates/demo-store/app/components/NotFound.tsx +22 -0
  33. package/dist/templates/demo-store/app/components/OrderCard.tsx +85 -0
  34. package/dist/templates/demo-store/app/components/Pagination.tsx +277 -0
  35. package/dist/templates/demo-store/app/components/ProductCard.tsx +146 -0
  36. package/dist/templates/demo-store/app/components/ProductGallery.tsx +114 -0
  37. package/dist/templates/demo-store/app/components/ProductGrid.tsx +93 -0
  38. package/dist/templates/demo-store/app/components/ProductSwimlane.tsx +30 -0
  39. package/dist/templates/demo-store/app/components/Skeleton.tsx +24 -0
  40. package/dist/templates/demo-store/app/components/SortFilter.tsx +411 -0
  41. package/dist/templates/demo-store/app/components/Text.tsx +192 -0
  42. package/dist/templates/demo-store/app/components/index.ts +28 -0
  43. package/dist/templates/demo-store/app/data/countries.ts +194 -0
  44. package/dist/templates/demo-store/app/data/index.ts +1037 -0
  45. package/dist/templates/demo-store/app/entry.client.tsx +4 -0
  46. package/dist/templates/demo-store/app/entry.server.tsx +26 -0
  47. package/dist/templates/demo-store/app/hooks/useCartFetchers.tsx +14 -0
  48. package/dist/templates/demo-store/app/hooks/useIsHydrated.tsx +12 -0
  49. package/dist/templates/demo-store/app/lib/const.ts +10 -0
  50. package/dist/templates/demo-store/app/lib/placeholders.ts +242 -0
  51. package/dist/templates/demo-store/app/lib/seo/common.tsx +324 -0
  52. package/dist/templates/demo-store/app/lib/seo/debugger.tsx +175 -0
  53. package/dist/templates/demo-store/app/lib/seo/image.tsx +32 -0
  54. package/dist/templates/demo-store/app/lib/seo/index.ts +4 -0
  55. package/dist/templates/demo-store/app/lib/seo/seo.tsx +24 -0
  56. package/dist/templates/demo-store/app/lib/seo/types.ts +70 -0
  57. package/dist/templates/demo-store/app/lib/session.server.ts +57 -0
  58. package/dist/templates/demo-store/app/lib/type.ts +21 -0
  59. package/dist/templates/demo-store/app/lib/utils.ts +310 -0
  60. package/dist/templates/demo-store/app/root.tsx +282 -0
  61. package/dist/templates/demo-store/app/routes/$.tsx +7 -0
  62. package/dist/templates/demo-store/app/routes/$lang/$.tsx +1 -0
  63. package/dist/templates/demo-store/app/routes/$lang/[robots.txt].tsx +1 -0
  64. package/dist/templates/demo-store/app/routes/$lang/[sitemap.xml].tsx +1 -0
  65. package/dist/templates/demo-store/app/routes/$lang/account/__private/address/$id.tsx +1 -0
  66. package/dist/templates/demo-store/app/routes/$lang/account/__private/edit.tsx +1 -0
  67. package/dist/templates/demo-store/app/routes/$lang/account/__private/logout.ts +1 -0
  68. package/dist/templates/demo-store/app/routes/$lang/account/__private/orders.$id.tsx +1 -0
  69. package/dist/templates/demo-store/app/routes/$lang/account/__public/activate.$id.$activationToken.tsx +6 -0
  70. package/dist/templates/demo-store/app/routes/$lang/account/__public/login.tsx +7 -0
  71. package/dist/templates/demo-store/app/routes/$lang/account/__public/recover.tsx +1 -0
  72. package/dist/templates/demo-store/app/routes/$lang/account/__public/register.tsx +6 -0
  73. package/dist/templates/demo-store/app/routes/$lang/account/__public/reset.$id.$resetToken.tsx +5 -0
  74. package/dist/templates/demo-store/app/routes/$lang/account.tsx +1 -0
  75. package/dist/templates/demo-store/app/routes/$lang/api/countries.tsx +1 -0
  76. package/dist/templates/demo-store/app/routes/$lang/api/products.tsx +1 -0
  77. package/dist/templates/demo-store/app/routes/$lang/cart.tsx +1 -0
  78. package/dist/templates/demo-store/app/routes/$lang/collections/$collectionHandle.tsx +6 -0
  79. package/dist/templates/demo-store/app/routes/$lang/collections/all.tsx +1 -0
  80. package/dist/templates/demo-store/app/routes/$lang/collections/index.tsx +1 -0
  81. package/dist/templates/demo-store/app/routes/$lang/featured-products.tsx +1 -0
  82. package/dist/templates/demo-store/app/routes/$lang/index.tsx +7 -0
  83. package/dist/templates/demo-store/app/routes/$lang/journal/$journalHandle.tsx +7 -0
  84. package/dist/templates/demo-store/app/routes/$lang/journal/index.tsx +1 -0
  85. package/dist/templates/demo-store/app/routes/$lang/og-image.tsx +1 -0
  86. package/dist/templates/demo-store/app/routes/$lang/pages/$pageHandle.tsx +1 -0
  87. package/dist/templates/demo-store/app/routes/$lang/policies/$policyHandle.tsx +1 -0
  88. package/dist/templates/demo-store/app/routes/$lang/policies/index.tsx +1 -0
  89. package/dist/templates/demo-store/app/routes/$lang/products/$productHandle.tsx +6 -0
  90. package/dist/templates/demo-store/app/routes/$lang/products/index.tsx +1 -0
  91. package/dist/templates/demo-store/app/routes/$lang/search.tsx +6 -0
  92. package/dist/templates/demo-store/app/routes/[robots.txt].tsx +40 -0
  93. package/dist/templates/demo-store/app/routes/[sitemap.xml].tsx +198 -0
  94. package/dist/templates/demo-store/app/routes/account/__private/address/$id.tsx +320 -0
  95. package/dist/templates/demo-store/app/routes/account/__private/edit.tsx +273 -0
  96. package/dist/templates/demo-store/app/routes/account/__private/logout.ts +29 -0
  97. package/dist/templates/demo-store/app/routes/account/__private/orders.$id.tsx +324 -0
  98. package/dist/templates/demo-store/app/routes/account/__public/activate.$id.$activationToken.tsx +218 -0
  99. package/dist/templates/demo-store/app/routes/account/__public/login.tsx +197 -0
  100. package/dist/templates/demo-store/app/routes/account/__public/recover.tsx +144 -0
  101. package/dist/templates/demo-store/app/routes/account/__public/register.tsx +184 -0
  102. package/dist/templates/demo-store/app/routes/account/__public/reset.$id.$resetToken.tsx +214 -0
  103. package/dist/templates/demo-store/app/routes/account.tsx +191 -0
  104. package/dist/templates/demo-store/app/routes/api/countries.tsx +22 -0
  105. package/dist/templates/demo-store/app/routes/api/products.tsx +116 -0
  106. package/dist/templates/demo-store/app/routes/cart.tsx +498 -0
  107. package/dist/templates/demo-store/app/routes/collections/$collectionHandle.tsx +308 -0
  108. package/dist/templates/demo-store/app/routes/collections/all.tsx +5 -0
  109. package/dist/templates/demo-store/app/routes/collections/index.tsx +195 -0
  110. package/dist/templates/demo-store/app/routes/discounts.$code.tsx +60 -0
  111. package/dist/templates/demo-store/app/routes/featured-products.tsx +58 -0
  112. package/dist/templates/demo-store/app/routes/index.tsx +254 -0
  113. package/dist/templates/demo-store/app/routes/journal/$journalHandle.tsx +147 -0
  114. package/dist/templates/demo-store/app/routes/journal/index.tsx +150 -0
  115. package/dist/templates/demo-store/app/routes/og-image.tsx +19 -0
  116. package/dist/templates/demo-store/app/routes/pages/$pageHandle.tsx +82 -0
  117. package/dist/templates/demo-store/app/routes/policies/$policyHandle.tsx +117 -0
  118. package/dist/templates/demo-store/app/routes/policies/index.tsx +104 -0
  119. package/dist/templates/demo-store/app/routes/products/$productHandle.tsx +561 -0
  120. package/dist/templates/demo-store/app/routes/products/index.tsx +155 -0
  121. package/dist/templates/demo-store/app/routes/search.tsx +205 -0
  122. package/dist/templates/demo-store/app/styles/custom-font.css +13 -0
  123. package/dist/templates/demo-store/package-lock.json +25515 -0
  124. package/dist/templates/demo-store/package.json +67 -0
  125. package/dist/templates/demo-store/playwright.config.ts +109 -0
  126. package/dist/templates/demo-store/postcss.config.js +10 -0
  127. package/dist/templates/demo-store/public/favicon.svg +28 -0
  128. package/dist/templates/demo-store/public/fonts/IBMPlexSerif-Text.woff2 +0 -0
  129. package/dist/templates/demo-store/public/fonts/IBMPlexSerif-TextItalic.woff2 +0 -0
  130. package/dist/templates/demo-store/remix.config.js +12 -0
  131. package/dist/templates/demo-store/remix.env.d.ts +34 -0
  132. package/dist/templates/demo-store/remix.init/index.ts +15 -0
  133. package/dist/templates/demo-store/remix.init/package.json +7 -0
  134. package/dist/templates/demo-store/server.ts +87 -0
  135. package/dist/templates/demo-store/styles/app.css +182 -0
  136. package/dist/templates/demo-store/tailwind.config.js +70 -0
  137. package/dist/templates/demo-store/tests/cart.test.ts +70 -0
  138. package/dist/templates/demo-store/tests/seo.test.ts +36 -0
  139. package/dist/templates/demo-store/tests/utils.ts +100 -0
  140. package/dist/templates/demo-store/tsconfig.json +26 -0
  141. package/dist/templates/hello-world/.eslintignore +4 -0
  142. package/dist/templates/hello-world/.eslintrc.js +6 -0
  143. package/dist/templates/hello-world/.graphqlrc.yml +1 -0
  144. package/dist/templates/hello-world/.turbo/turbo-build.log +9 -0
  145. package/dist/templates/hello-world/README.md +20 -0
  146. package/dist/templates/hello-world/app/components/Layout.tsx +15 -0
  147. package/dist/templates/hello-world/app/components/index.ts +1 -0
  148. package/dist/templates/hello-world/app/entry.client.tsx +4 -0
  149. package/dist/templates/hello-world/app/entry.server.tsx +21 -0
  150. package/dist/templates/hello-world/app/root.tsx +212 -0
  151. package/dist/templates/hello-world/app/routes/index.tsx +7 -0
  152. package/dist/templates/hello-world/app/styles/app.css +38 -0
  153. package/dist/templates/hello-world/package-lock.json +27641 -0
  154. package/dist/templates/hello-world/package.json +41 -0
  155. package/dist/templates/hello-world/public/favicon.svg +28 -0
  156. package/dist/templates/hello-world/remix.env.d.ts +29 -0
  157. package/dist/templates/hello-world/server.ts +127 -0
  158. package/dist/templates/hello-world/tsconfig.json +25 -0
  159. package/dist/utils/config.js +81 -0
  160. package/dist/utils/flags.js +15 -0
  161. package/dist/utils/log.js +20 -0
  162. package/dist/utils/mini-oxygen.js +70 -0
  163. package/package.json +27 -64
  164. package/tmp-create-app.mjs +29 -0
  165. package/LICENSE +0 -8
  166. package/README.md +0 -61
  167. package/dist/cli/commands/hydrogen/add/eslint.d.ts +0 -11
  168. package/dist/cli/commands/hydrogen/add/eslint.js +0 -26
  169. package/dist/cli/commands/hydrogen/add/eslint.js.map +0 -1
  170. package/dist/cli/commands/hydrogen/add/tailwind.d.ts +0 -11
  171. package/dist/cli/commands/hydrogen/add/tailwind.js +0 -26
  172. package/dist/cli/commands/hydrogen/add/tailwind.js.map +0 -1
  173. package/dist/cli/commands/hydrogen/build.d.ts +0 -14
  174. package/dist/cli/commands/hydrogen/build.js +0 -49
  175. package/dist/cli/commands/hydrogen/build.js.map +0 -1
  176. package/dist/cli/commands/hydrogen/deploy.d.ts +0 -19
  177. package/dist/cli/commands/hydrogen/deploy.js +0 -58
  178. package/dist/cli/commands/hydrogen/deploy.js.map +0 -1
  179. package/dist/cli/commands/hydrogen/dev.d.ts +0 -13
  180. package/dist/cli/commands/hydrogen/dev.js +0 -31
  181. package/dist/cli/commands/hydrogen/dev.js.map +0 -1
  182. package/dist/cli/commands/hydrogen/info.d.ts +0 -12
  183. package/dist/cli/commands/hydrogen/info.js +0 -28
  184. package/dist/cli/commands/hydrogen/info.js.map +0 -1
  185. package/dist/cli/commands/hydrogen/preview.d.ts +0 -13
  186. package/dist/cli/commands/hydrogen/preview.js +0 -46
  187. package/dist/cli/commands/hydrogen/preview.js.map +0 -1
  188. package/dist/cli/constants.d.ts +0 -15
  189. package/dist/cli/constants.js +0 -16
  190. package/dist/cli/constants.js.map +0 -1
  191. package/dist/cli/flags.d.ts +0 -4
  192. package/dist/cli/flags.js +0 -16
  193. package/dist/cli/flags.js.map +0 -1
  194. package/dist/cli/models/hydrogen.d.ts +0 -22
  195. package/dist/cli/models/hydrogen.js +0 -82
  196. package/dist/cli/models/hydrogen.js.map +0 -1
  197. package/dist/cli/prompts/git-init.d.ts +0 -1
  198. package/dist/cli/prompts/git-init.js +0 -16
  199. package/dist/cli/prompts/git-init.js.map +0 -1
  200. package/dist/cli/services/build/check-lockfile.d.ts +0 -3
  201. package/dist/cli/services/build/check-lockfile.js +0 -80
  202. package/dist/cli/services/build/check-lockfile.js.map +0 -1
  203. package/dist/cli/services/build.d.ts +0 -14
  204. package/dist/cli/services/build.js +0 -44
  205. package/dist/cli/services/build.js.map +0 -1
  206. package/dist/cli/services/deploy/config.d.ts +0 -4
  207. package/dist/cli/services/deploy/config.js +0 -49
  208. package/dist/cli/services/deploy/config.js.map +0 -1
  209. package/dist/cli/services/deploy/error.d.ts +0 -4
  210. package/dist/cli/services/deploy/error.js +0 -11
  211. package/dist/cli/services/deploy/error.js.map +0 -1
  212. package/dist/cli/services/deploy/graphql/create_deployment.d.ts +0 -10
  213. package/dist/cli/services/deploy/graphql/create_deployment.js +0 -15
  214. package/dist/cli/services/deploy/graphql/create_deployment.js.map +0 -1
  215. package/dist/cli/services/deploy/graphql/upload_deployment.d.ts +0 -1
  216. package/dist/cli/services/deploy/graphql/upload_deployment.js +0 -16
  217. package/dist/cli/services/deploy/graphql/upload_deployment.js.map +0 -1
  218. package/dist/cli/services/deploy/types.d.ts +0 -37
  219. package/dist/cli/services/deploy/types.js +0 -2
  220. package/dist/cli/services/deploy/types.js.map +0 -1
  221. package/dist/cli/services/deploy/upload.d.ts +0 -5
  222. package/dist/cli/services/deploy/upload.js +0 -81
  223. package/dist/cli/services/deploy/upload.js.map +0 -1
  224. package/dist/cli/services/deploy.d.ts +0 -2
  225. package/dist/cli/services/deploy.js +0 -103
  226. package/dist/cli/services/deploy.js.map +0 -1
  227. package/dist/cli/services/dev/check-version.d.ts +0 -1
  228. package/dist/cli/services/dev/check-version.js +0 -30
  229. package/dist/cli/services/dev/check-version.js.map +0 -1
  230. package/dist/cli/services/dev.d.ts +0 -10
  231. package/dist/cli/services/dev.js +0 -36
  232. package/dist/cli/services/dev.js.map +0 -1
  233. package/dist/cli/services/eslint.d.ts +0 -8
  234. package/dist/cli/services/eslint.js +0 -74
  235. package/dist/cli/services/eslint.js.map +0 -1
  236. package/dist/cli/services/info.d.ts +0 -7
  237. package/dist/cli/services/info.js +0 -131
  238. package/dist/cli/services/info.js.map +0 -1
  239. package/dist/cli/services/preview.d.ts +0 -12
  240. package/dist/cli/services/preview.js +0 -63
  241. package/dist/cli/services/preview.js.map +0 -1
  242. package/dist/cli/services/tailwind.d.ts +0 -9
  243. package/dist/cli/services/tailwind.js +0 -103
  244. package/dist/cli/services/tailwind.js.map +0 -1
  245. package/dist/cli/utilities/load-config.d.ts +0 -5
  246. package/dist/cli/utilities/load-config.js +0 -6
  247. package/dist/cli/utilities/load-config.js.map +0 -1
  248. package/dist/tsconfig.tsbuildinfo +0 -1
  249. package/oclif.manifest.json +0 -1
@@ -0,0 +1,175 @@
1
+ import {Link, useLocation, type RouteMatch} from '@remix-run/react';
2
+ import {useSeoConfig, useHeadTags, recursivelyInvokeOrReturn} from './common';
3
+ import {renderToString} from 'react-dom/server';
4
+
5
+ const LABEL_MAP = {
6
+ twitterTags: 'Twitter',
7
+ ogTags: 'Open graph',
8
+ links: 'Links',
9
+ tags: 'Meta',
10
+ LdJson: 'Structured data',
11
+ };
12
+
13
+ function Badge() {
14
+ return (
15
+ <div className="fixed bottom-5 right-5 divide-y rounded-md bg-white">
16
+ <div className="flex gap-2 px-3 py-3 items-center px-5 py-3">
17
+ <Link
18
+ to="?debug=true"
19
+ reloadDocument
20
+ className="flex-1 text-sm font-bold text-gray-600"
21
+ >
22
+ SEO
23
+ </Link>
24
+ <span className="absolute bottom-0 right-0 h-3 w-3 rounded-full bg-green-500 ring ring-white"></span>
25
+ </div>
26
+ </div>
27
+ );
28
+ }
29
+
30
+ export function Debugger() {
31
+ const {seo, matches} = useSeoConfig();
32
+
33
+ const tags = useHeadTags(seo);
34
+ const location = useLocation();
35
+ const debug = new URLSearchParams(location.search).get('debug');
36
+ const debuggerOpen = Boolean(
37
+ debug === 'false' ? false : debug === '' || debug,
38
+ );
39
+
40
+ return debuggerOpen ? (
41
+ <Panel>
42
+ <>
43
+ <span className="font-bold block text-sm py-4 px-4">Config</span>
44
+ {Object.entries(seo).flatMap(([property, value]) => {
45
+ if (typeof value !== 'string') {
46
+ return null;
47
+ }
48
+ return (
49
+ <Item
50
+ key={`${property}${value}`}
51
+ property={property}
52
+ value={value}
53
+ />
54
+ );
55
+ })}
56
+
57
+ <div className="py-4 px-4 space-y-4">
58
+ {matches.map(({id, handle, data}: RouteMatch, index: number) => (
59
+ <div key={id}>
60
+ <div className="font-bold text-xs pb-2 ">{id}</div>
61
+ <pre className="overflow-x-scroll whitespace-pre font-mono rounded-sm bg-gray-100 text-[10px] px-4 py-2">
62
+ {JSON.stringify(
63
+ recursivelyInvokeOrReturn(handle?.seo, data),
64
+ null,
65
+ 2,
66
+ )}
67
+ </pre>
68
+ </div>
69
+ ))}
70
+ </div>
71
+
72
+ <span className="font-bold block text-sm py-4 px-4">Tags</span>
73
+
74
+ {Object.entries(tags).map(([label, entries]) => {
75
+ if (entries.length < 1) {
76
+ return null;
77
+ }
78
+ return (
79
+ <div key={label} className="font-bold text-sm py-4 px-4">
80
+ <div className="font-bold text-xs pb-2">
81
+ {LABEL_MAP[label as keyof typeof LABEL_MAP]}
82
+ </div>
83
+ <pre className="overflow-x-scroll whitespace-pre font-mono rounded-sm bg-gray-100 text-[10px] px-4 py-2">
84
+ {label === 'LdJson'
85
+ ? JSON.stringify(entries, null, 2)
86
+ : entries.map(
87
+ (entry: React.ReactElement, index: number) =>
88
+ renderToString(entry) + '\n',
89
+ )}
90
+ </pre>
91
+ </div>
92
+ );
93
+ })}
94
+ </>
95
+ </Panel>
96
+ ) : (
97
+ <Badge />
98
+ );
99
+ }
100
+
101
+ function Panel({children}: {children?: React.ReactNode}) {
102
+ return (
103
+ <div className="overflow-y-scroll height max-h-full z-40 fixed w-96 bottom-5 top-5 right-5 divide-y rounded-md bg-white text-gray-600">
104
+ <div className="flex items-center px-4 py-3 ">
105
+ <span className="flex-1 text-sm font-bold text-gray-600">SEO</span>
106
+ <Link to="?" reloadDocument className="text-sm font-bold text-gray-600">
107
+ <svg
108
+ xmlns="http://www.w3.org/2000/svg"
109
+ viewBox="0 0 20 20"
110
+ fill="currentColor"
111
+ className="h-5 w-5"
112
+ >
113
+ <path
114
+ fillRule="evenodd"
115
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
116
+ clipRule="evenodd"
117
+ />
118
+ </svg>
119
+ </Link>
120
+ </div>
121
+ {children}
122
+ </div>
123
+ );
124
+ }
125
+
126
+ function Item({
127
+ pass,
128
+ value,
129
+ property,
130
+ }: {
131
+ pass?: boolean;
132
+ value: string;
133
+ property: string;
134
+ }) {
135
+ const icon =
136
+ pass !== false ? (
137
+ <svg
138
+ xmlns="http://www.w3.org/2000/svg"
139
+ className="w-5 h-5 top-0.5 relative text-blue-500"
140
+ viewBox="0 0 20 20"
141
+ fill="currentColor"
142
+ >
143
+ <path
144
+ fillRule="evenodd"
145
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
146
+ clipRule="evenodd"
147
+ />
148
+ </svg>
149
+ ) : (
150
+ <svg
151
+ xmlns="http://www.w3.org/2000/svg"
152
+ className="w-5 h-5 top-0.5 relative text-red-500"
153
+ viewBox="0 0 20 20"
154
+ fill="currentColor"
155
+ >
156
+ <path
157
+ fillRule="evenodd"
158
+ d="M13.477 14.89A6 6 0 015.11 6.524l8.367 8.368zm1.414-1.414L6.524 5.11a6 6 0 018.367 8.367zM18 10a8 8 0 11-16 0 8 8 0 0116 0z"
159
+ clipRule="evenodd"
160
+ />
161
+ </svg>
162
+ );
163
+
164
+ return (
165
+ <div className="flex items-end px-4 py-3 ">
166
+ {icon}
167
+ <span className="font-mono flex-1 mx-1 text-gray-900 text-[10px]">
168
+ <span className="px-2 py-1 rounded-sm bg-gray-100">{property}</span>
169
+ </span>
170
+ <span className="pl-8 text-right text-xs text-gray-900 text-ellipsis overflow-hidden whitespace-nowrap max-w-full">
171
+ {value}
172
+ </span>
173
+ </div>
174
+ );
175
+ }
@@ -0,0 +1,32 @@
1
+ import {renderToString} from 'react-dom/server';
2
+
3
+ export async function getShareableImage(
4
+ component: React.ReactElement<any, 'svg'>,
5
+ ) {
6
+ try {
7
+ const svg = renderToString(component);
8
+ return new Response(svg, {
9
+ headers: {
10
+ 'Content-Type': 'image/svg+xml',
11
+ 'Cache-Control': 'public, s-maxage=60',
12
+ },
13
+ });
14
+ } catch (error) {
15
+ // eslint-disable-next-line no-console
16
+ console.error('Failed to render Share Image:', component, error);
17
+
18
+ return new Response(
19
+ renderToString(
20
+ <svg>
21
+ <text>Default share image</text>
22
+ </svg>,
23
+ ),
24
+ {
25
+ headers: {
26
+ 'Content-Type': 'image/svg+xml',
27
+ 'Cache-Control': 'public, s-maxage=60',
28
+ },
29
+ },
30
+ );
31
+ }
32
+ }
@@ -0,0 +1,4 @@
1
+ export {Seo} from './seo';
2
+ export {Debugger} from './debugger';
3
+
4
+ export * from './types';
@@ -0,0 +1,24 @@
1
+ import {useSeoConfig, useHeadTags} from './common';
2
+
3
+ export function Seo() {
4
+ const {seo} = useSeoConfig();
5
+ const {tags, ogTags, twitterTags, links, LdJson} = useHeadTags(seo);
6
+ const structuredContent = (
7
+ <script
8
+ type="application/ld+json"
9
+ dangerouslySetInnerHTML={{
10
+ __html: JSON.stringify(LdJson),
11
+ }}
12
+ />
13
+ );
14
+
15
+ return (
16
+ <>
17
+ {tags}
18
+ {ogTags}
19
+ {twitterTags}
20
+ {links}
21
+ {structuredContent}
22
+ </>
23
+ );
24
+ }
@@ -0,0 +1,70 @@
1
+ export type SeoDescriptor = {
2
+ type: 'product' | 'article' | 'page' | 'collection' | 'blog' | 'root';
3
+ site: string;
4
+ title: string;
5
+ titleTemplate?: string;
6
+ bypassTitleTemplate?: boolean;
7
+ defaultTitle: string;
8
+ description: string;
9
+ noindex: boolean;
10
+ nofollow: boolean;
11
+ url: string;
12
+ twitter: Partial<TwitterOptions>;
13
+ openGraph: Partial<OpenGraphOptions>;
14
+ images: Partial<ImageOptions>[];
15
+ alternates: Partial<AlternateOptions>[];
16
+ tags: string[];
17
+ robots: Partial<RobotsOptions>;
18
+ };
19
+
20
+ export interface TwitterOptions {
21
+ card: string;
22
+ site: string;
23
+ description: string;
24
+ handle: string;
25
+ }
26
+
27
+ export interface OpenGraphOptions {
28
+ url: string;
29
+ type: string;
30
+ title: string;
31
+ description: string;
32
+ site: string;
33
+ locale: string;
34
+ }
35
+
36
+ export interface OpenGraphProfileOptions {
37
+ firstName: string;
38
+ lastName: string;
39
+ username: string;
40
+ gender: string;
41
+ }
42
+
43
+ export interface OpenGraphArticleOptions {
44
+ publishedTime: Date;
45
+ modifiedTime: Date;
46
+ expirationTime: Date;
47
+ authors: string[];
48
+ section: string;
49
+ tags: string;
50
+ }
51
+
52
+ export interface RobotsOptions {
53
+ noArchive: boolean;
54
+ noSnippet: boolean;
55
+ maxSnippet: number;
56
+ unAvailableAfter: string;
57
+ }
58
+
59
+ export interface AlternateOptions {
60
+ url: string;
61
+ media: string;
62
+ lang: string;
63
+ }
64
+
65
+ export interface ImageOptions {
66
+ url: string;
67
+ width: number;
68
+ height: number;
69
+ alt: string;
70
+ }
@@ -0,0 +1,57 @@
1
+ import {
2
+ createCookieSessionStorage,
3
+ type SessionStorage,
4
+ type Session,
5
+ } from '@shopify/remix-oxygen';
6
+
7
+ /**
8
+ * This is a custom session implementation for your Hydrogen shop.
9
+ * Feel free to customize it to your needs, add helper methods, or
10
+ * swap out the cookie-based implementation with something else!
11
+ */
12
+ export class HydrogenSession {
13
+ constructor(
14
+ private sessionStorage: SessionStorage,
15
+ private session: Session,
16
+ ) {}
17
+
18
+ static async init(request: Request, secrets: string[]) {
19
+ const storage = createCookieSessionStorage({
20
+ cookie: {
21
+ name: 'session',
22
+ httpOnly: true,
23
+ path: '/',
24
+ sameSite: 'lax',
25
+ secrets,
26
+ },
27
+ });
28
+
29
+ const session = await storage.getSession(request.headers.get('Cookie'));
30
+
31
+ return new this(storage, session);
32
+ }
33
+
34
+ get(key: string) {
35
+ return this.session.get(key);
36
+ }
37
+
38
+ destroy() {
39
+ return this.sessionStorage.destroySession(this.session);
40
+ }
41
+
42
+ flash(key: string, value: any) {
43
+ this.session.flash(key, value);
44
+ }
45
+
46
+ unset(key: string) {
47
+ this.session.unset(key);
48
+ }
49
+
50
+ set(key: string, value: any) {
51
+ this.session.set(key, value);
52
+ }
53
+
54
+ commit() {
55
+ return this.sessionStorage.commitSession(this.session);
56
+ }
57
+ }
@@ -0,0 +1,21 @@
1
+ import {
2
+ CountryCode,
3
+ LanguageCode,
4
+ } from '@shopify/hydrogen-react/storefront-api-types';
5
+
6
+ export type Locale = {
7
+ label?: string;
8
+ language: LanguageCode;
9
+ country: CountryCode;
10
+ };
11
+
12
+ export type Localizations = Record<string, Locale>;
13
+
14
+ export enum CartAction {
15
+ ADD_TO_CART = 'ADD_TO_CART',
16
+ REMOVE_FROM_CART = 'REMOVE_FROM_CART',
17
+ UPDATE_CART = 'UPDATE_CART',
18
+ UPDATE_DISCOUNT = 'UPDATE_DISCOUNT',
19
+ UPDATE_BUYER_IDENTITY = 'UPDATE_BUYER_IDENTITY',
20
+ }
21
+ export type CartActions = keyof typeof CartAction;
@@ -0,0 +1,310 @@
1
+ import {useLocation, useMatches} from '@remix-run/react';
2
+ import type {
3
+ MenuItem,
4
+ Menu,
5
+ MoneyV2,
6
+ } from '@shopify/hydrogen-react/storefront-api-types';
7
+
8
+ // @ts-expect-error types not available
9
+ import typographicBase from 'typographic-base';
10
+ import {countries} from '~/data/countries';
11
+ import {Locale} from './type';
12
+
13
+ export interface EnhancedMenuItem extends MenuItem {
14
+ to: string;
15
+ target: string;
16
+ isExternal?: boolean;
17
+ items: EnhancedMenuItem[];
18
+ }
19
+
20
+ export interface EnhancedMenu extends Menu {
21
+ items: EnhancedMenuItem[];
22
+ }
23
+
24
+ export function missingClass(string?: string, prefix?: string) {
25
+ if (!string) {
26
+ return true;
27
+ }
28
+
29
+ const regex = new RegExp(` ?${prefix}`, 'g');
30
+ return string.match(regex) === null;
31
+ }
32
+
33
+ export function formatText(input?: string | React.ReactNode) {
34
+ if (!input) {
35
+ return;
36
+ }
37
+
38
+ if (typeof input !== 'string') {
39
+ return input;
40
+ }
41
+
42
+ return typographicBase(input, {locale: 'en-us'}).replace(
43
+ /\s([^\s<]+)\s*$/g,
44
+ '\u00A0$1',
45
+ );
46
+ }
47
+
48
+ export function getExcerpt(text: string) {
49
+ const regex = /<p.*>(.*?)<\/p>/;
50
+ const match = regex.exec(text);
51
+ return match?.length ? match[0] : text;
52
+ }
53
+
54
+ export function isNewArrival(date: string, daysOld = 30) {
55
+ return (
56
+ new Date(date).valueOf() >
57
+ new Date().setDate(new Date().getDate() - daysOld).valueOf()
58
+ );
59
+ }
60
+
61
+ export function isDiscounted(price: MoneyV2, compareAtPrice: MoneyV2) {
62
+ if (compareAtPrice?.amount > price?.amount) {
63
+ return true;
64
+ }
65
+ return false;
66
+ }
67
+
68
+ function resolveToFromType(
69
+ {
70
+ customPrefixes,
71
+ pathname,
72
+ type,
73
+ }: {
74
+ customPrefixes: Record<string, string>;
75
+ pathname?: string;
76
+ type?: string;
77
+ } = {
78
+ customPrefixes: {},
79
+ },
80
+ ) {
81
+ if (!pathname || !type) return '';
82
+
83
+ /*
84
+ MenuItemType enum
85
+ @see: https://shopify.dev/api/storefront/unstable/enums/MenuItemType
86
+ */
87
+ const defaultPrefixes = {
88
+ BLOG: 'blogs',
89
+ COLLECTION: 'collections',
90
+ COLLECTIONS: 'collections', // Collections All (not documented)
91
+ FRONTPAGE: 'frontpage',
92
+ HTTP: '',
93
+ PAGE: 'pages',
94
+ CATALOG: 'collections/all', // Products All
95
+ PRODUCT: 'products',
96
+ SEARCH: 'search',
97
+ SHOP_POLICY: 'policies',
98
+ };
99
+
100
+ const pathParts = pathname.split('/');
101
+ const handle = pathParts.pop() || '';
102
+ const routePrefix: Record<string, string> = {
103
+ ...defaultPrefixes,
104
+ ...customPrefixes,
105
+ };
106
+
107
+ switch (true) {
108
+ // special cases
109
+ case type === 'FRONTPAGE':
110
+ return '/';
111
+
112
+ case type === 'ARTICLE': {
113
+ const blogHandle = pathParts.pop();
114
+ return routePrefix.BLOG
115
+ ? `/${routePrefix.BLOG}/${blogHandle}/${handle}/`
116
+ : `/${blogHandle}/${handle}/`;
117
+ }
118
+
119
+ case type === 'COLLECTIONS':
120
+ return `/${routePrefix.COLLECTIONS}`;
121
+
122
+ case type === 'SEARCH':
123
+ return `/${routePrefix.SEARCH}`;
124
+
125
+ case type === 'CATALOG':
126
+ return `/${routePrefix.CATALOG}`;
127
+
128
+ // common cases: BLOG, PAGE, COLLECTION, PRODUCT, SHOP_POLICY, HTTP
129
+ default:
130
+ return routePrefix[type]
131
+ ? `/${routePrefix[type]}/${handle}`
132
+ : `/${handle}`;
133
+ }
134
+ }
135
+
136
+ /*
137
+ Parse each menu link and adding, isExternal, to and target
138
+ */
139
+ function parseItem(customPrefixes = {}) {
140
+ return function (item: MenuItem): EnhancedMenuItem {
141
+ if (!item?.url || !item?.type) {
142
+ // eslint-disable-next-line no-console
143
+ console.warn('Invalid menu item. Must include a url and type.');
144
+ // @ts-ignore
145
+ return;
146
+ }
147
+
148
+ // extract path from url because we don't need the origin on internal to attributes
149
+ const {pathname} = new URL(item.url);
150
+
151
+ /*
152
+ Currently the MenuAPI only returns online store urls e.g — xyz.myshopify.com/..
153
+ Note: update logic when API is updated to include the active qualified domain
154
+ */
155
+ const isInternalLink = /\.myshopify\.com/g.test(item.url);
156
+
157
+ const parsedItem = isInternalLink
158
+ ? // internal links
159
+ {
160
+ ...item,
161
+ isExternal: false,
162
+ target: '_self',
163
+ to: resolveToFromType({type: item.type, customPrefixes, pathname}),
164
+ }
165
+ : // external links
166
+ {
167
+ ...item,
168
+ isExternal: true,
169
+ target: '_blank',
170
+ to: item.url,
171
+ };
172
+
173
+ return {
174
+ ...parsedItem,
175
+ items: item.items?.map(parseItem(customPrefixes)),
176
+ };
177
+ };
178
+ }
179
+
180
+ /*
181
+ Recursively adds `to` and `target` attributes to links based on their url
182
+ and resource type.
183
+ It optionally overwrites url paths based on item.type
184
+ */
185
+ export function parseMenu(menu: Menu, customPrefixes = {}): EnhancedMenu {
186
+ if (!menu?.items) {
187
+ // eslint-disable-next-line no-console
188
+ console.warn('Invalid menu passed to parseMenu');
189
+ // @ts-ignore
190
+ return menu;
191
+ }
192
+
193
+ return {
194
+ ...menu,
195
+ items: menu.items.map(parseItem(customPrefixes)),
196
+ };
197
+ }
198
+
199
+ export const INPUT_STYLE_CLASSES =
200
+ 'appearance-none rounded dark:bg-transparent border focus:border-primary/50 focus:ring-0 w-full py-2 px-3 text-primary/90 placeholder:text-primary/50 leading-tight focus:shadow-outline';
201
+
202
+ export const getInputStyleClasses = (isError?: string | null) => {
203
+ return `${INPUT_STYLE_CLASSES} ${
204
+ isError ? 'border-red-500' : 'border-primary/20'
205
+ }`;
206
+ };
207
+
208
+ export function statusMessage(status: string) {
209
+ const translations: Record<string, string> = {
210
+ ATTEMPTED_DELIVERY: 'Attempted delivery',
211
+ CANCELED: 'Canceled',
212
+ CONFIRMED: 'Confirmed',
213
+ DELIVERED: 'Delivered',
214
+ FAILURE: 'Failure',
215
+ FULFILLED: 'Fulfilled',
216
+ IN_PROGRESS: 'In Progress',
217
+ IN_TRANSIT: 'In transit',
218
+ LABEL_PRINTED: 'Label printed',
219
+ LABEL_PURCHASED: 'Label purchased',
220
+ LABEL_VOIDED: 'Label voided',
221
+ MARKED_AS_FULFILLED: 'Marked as fulfilled',
222
+ NOT_DELIVERED: 'Not delivered',
223
+ ON_HOLD: 'On Hold',
224
+ OPEN: 'Open',
225
+ OUT_FOR_DELIVERY: 'Out for delivery',
226
+ PARTIALLY_FULFILLED: 'Partially Fulfilled',
227
+ PENDING_FULFILLMENT: 'Pending',
228
+ PICKED_UP: 'Displayed as Picked up',
229
+ READY_FOR_PICKUP: 'Ready for pickup',
230
+ RESTOCKED: 'Restocked',
231
+ SCHEDULED: 'Scheduled',
232
+ SUBMITTED: 'Submitted',
233
+ UNFULFILLED: 'Unfulfilled',
234
+ };
235
+ try {
236
+ return translations?.[status];
237
+ } catch (error) {
238
+ return status;
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Errors can exist in an errors object, or nested in a data field.
244
+ */
245
+ export function assertApiErrors(data: Record<string, any> | null | undefined) {
246
+ const errorMessage = data?.customerUserErrors?.[0]?.message;
247
+ if (errorMessage) {
248
+ throw new Error(errorMessage);
249
+ }
250
+ }
251
+
252
+ export const DEFAULT_LOCALE: Locale & {pathPrefix: string} = Object.freeze({
253
+ ...countries.default,
254
+ pathPrefix: '',
255
+ });
256
+
257
+ export function getLocaleFromRequest(request: Request): Locale & {
258
+ pathPrefix: string;
259
+ } {
260
+ const url = new URL(request.url);
261
+ const firstPathPart =
262
+ '/' + url.pathname.substring(1).split('/')[0].toLowerCase();
263
+
264
+ return countries[firstPathPart]
265
+ ? {
266
+ ...countries[firstPathPart],
267
+ pathPrefix: firstPathPart,
268
+ }
269
+ : {
270
+ ...countries['default'],
271
+ pathPrefix: '',
272
+ };
273
+ }
274
+
275
+ export function usePrefixPathWithLocale(path: string) {
276
+ const [root] = useMatches();
277
+ const selectedLocale = root.data?.selectedLocale ?? DEFAULT_LOCALE;
278
+
279
+ return `${selectedLocale.pathPrefix}${
280
+ path.startsWith('/') ? path : '/' + path
281
+ }`;
282
+ }
283
+
284
+ export function useIsHomePath() {
285
+ const {pathname} = useLocation();
286
+ const [root] = useMatches();
287
+ const selectedLocale = root.data?.selectedLocale ?? DEFAULT_LOCALE;
288
+ const strippedPathname = pathname.replace(selectedLocale.pathPrefix, '');
289
+ return strippedPathname === '/';
290
+ }
291
+
292
+ /**
293
+ * Validates that a url is local
294
+ * @param url
295
+ * @returns `true` if local `false`if external domain
296
+ */
297
+ export function isLocalPath(url: string) {
298
+ try {
299
+ // We don't want to redirect cross domain,
300
+ // doing so could create fishing vulnerability
301
+ // If `new URL()` succeeds, it's a fully qualified
302
+ // url which is cross domain. If it fails, it's just
303
+ // a path, which will be the current domain.
304
+ new URL(url);
305
+ } catch (e) {
306
+ return true;
307
+ }
308
+
309
+ return false;
310
+ }