@shopify/hydrogen 0.13.0 → 0.14.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 (57) hide show
  1. package/CHANGELOG.md +51 -5
  2. package/dist/esnext/components/Link/Link.client.d.ts +6 -0
  3. package/dist/esnext/components/Link/Link.client.js +85 -3
  4. package/dist/esnext/components/LocalizationProvider/LocalizationContext.client.d.ts +1 -1
  5. package/dist/esnext/components/LocalizationProvider/LocalizationProvider.server.d.ts +16 -0
  6. package/dist/esnext/components/LocalizationProvider/LocalizationProvider.server.js +7 -2
  7. package/dist/esnext/components/Seo/DefaultPageSeo.client.js +1 -2
  8. package/dist/esnext/entry-client.js +14 -1
  9. package/dist/esnext/entry-server.d.ts +1 -0
  10. package/dist/esnext/entry-server.js +15 -9
  11. package/dist/esnext/foundation/ServerRequestProvider/ServerRequestProvider.js +1 -1
  12. package/dist/esnext/foundation/ServerStateProvider/ServerStateProvider.d.ts +6 -1
  13. package/dist/esnext/foundation/ServerStateProvider/ServerStateProvider.js +27 -22
  14. package/dist/esnext/foundation/ShopifyProvider/ShopifyProvider.server.js +4 -1
  15. package/dist/esnext/foundation/ShopifyProvider/types.d.ts +3 -1
  16. package/dist/esnext/foundation/constants.d.ts +1 -1
  17. package/dist/esnext/foundation/constants.js +1 -1
  18. package/dist/esnext/foundation/useQuery/hooks.d.ts +3 -0
  19. package/dist/esnext/foundation/useQuery/hooks.js +8 -2
  20. package/dist/esnext/framework/Hydration/ServerComponentRequest.server.d.ts +7 -0
  21. package/dist/esnext/framework/Hydration/ServerComponentRequest.server.js +24 -7
  22. package/dist/esnext/framework/cache/in-memory.d.ts +1 -0
  23. package/dist/esnext/framework/cache/in-memory.js +15 -5
  24. package/dist/esnext/framework/cache.d.ts +0 -8
  25. package/dist/esnext/framework/cache.js +0 -8
  26. package/dist/esnext/hooks/useParsedMetafields/useParsedMetafields.d.ts +1 -1
  27. package/dist/esnext/hooks/useShopQuery/hooks.d.ts +1 -1
  28. package/dist/esnext/hooks/useShopQuery/hooks.js +33 -12
  29. package/dist/esnext/platforms/node.d.ts +2 -3
  30. package/dist/esnext/platforms/node.js +5 -3
  31. package/dist/esnext/platforms/worker.js +2 -1
  32. package/dist/esnext/storefront-api-types.d.ts +150 -3
  33. package/dist/esnext/storefront-api-types.js +16 -0
  34. package/dist/esnext/utilities/apiRoutes.js +3 -1
  35. package/dist/esnext/utilities/flattenConnection/flattenConnection.js +2 -5
  36. package/dist/esnext/version.d.ts +1 -1
  37. package/dist/esnext/version.js +1 -1
  38. package/dist/node/entry-server.d.ts +1 -0
  39. package/dist/node/entry-server.js +15 -9
  40. package/dist/node/foundation/ServerRequestProvider/ServerRequestProvider.js +1 -1
  41. package/dist/node/foundation/ServerStateProvider/ServerStateProvider.d.ts +6 -1
  42. package/dist/node/foundation/ServerStateProvider/ServerStateProvider.js +27 -22
  43. package/dist/node/foundation/ShopifyProvider/types.d.ts +3 -1
  44. package/dist/node/framework/Hydration/ServerComponentRequest.server.d.ts +7 -0
  45. package/dist/node/framework/Hydration/ServerComponentRequest.server.js +24 -7
  46. package/dist/node/framework/cache/in-memory.d.ts +1 -0
  47. package/dist/node/framework/cache/in-memory.js +15 -5
  48. package/dist/node/framework/cache.d.ts +0 -8
  49. package/dist/node/framework/cache.js +1 -10
  50. package/dist/node/storefront-api-types.d.ts +150 -3
  51. package/dist/node/storefront-api-types.js +17 -1
  52. package/dist/node/utilities/apiRoutes.js +3 -1
  53. package/dist/node/version.d.ts +1 -1
  54. package/dist/node/version.js +1 -1
  55. package/package.json +3 -3
  56. package/dist/esnext/components/LocalizationProvider/LocalizationQuery.d.ts +0 -23
  57. package/dist/esnext/components/LocalizationProvider/LocalizationQuery.js +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1028](https://github.com/Shopify/hydrogen/pull/1028) [`ba174588`](https://github.com/Shopify/hydrogen/commit/ba174588d8f4a9f1054779a9bf32a92e8d2c921c) Thanks [@michenly](https://github.com/michenly)! - Starting from SF API version `2022-04`, the preferred way to request translatable resources is using the `@inContext` directive. See the [API docs](https://shopify.dev/api/examples/multiple-languages#retrieve-translations-with-the-storefront-api) on how to do this and which resources have translatable properties.
8
+
9
+ This causes a breaking change to the `useShopQuery` hook. The `locale` property has been removed from the argument object; `Accept-Language` is no longer being send with every request, and we are no longer using locale as part of the cache key.
10
+
11
+ The `useShop` hook will now return the `languageCode` key, which is the first two characters of the existing `locale` key.
12
+
13
+ Both `locale` & `languageCode` values are also now capitalized to make it easier to pass into a GraphQL `@inContext` directive.
14
+
15
+ * [#1020](https://github.com/Shopify/hydrogen/pull/1020) [`e9529bc8`](https://github.com/Shopify/hydrogen/commit/e9529bc81410e0d99f9d3dbdb138ae61d00f876b) Thanks [@jplhomer](https://github.com/jplhomer)! - Preload `Link` URLs by default when a user signals intent to visit the URL. This includes hovering or focusing on the URL. To disable preloading, pass `<Link preload={false} />` to the component.
16
+
17
+ ### Patch Changes
18
+
19
+ - [#1017](https://github.com/Shopify/hydrogen/pull/1017) [`4c87fb63`](https://github.com/Shopify/hydrogen/commit/4c87fb639a79da883f99c58acde0d17c713c7620) Thanks [@frandiox](https://github.com/frandiox)! - Do not cache Storefront API responses that contain GraphQL errors (amend previous fix).
20
+
21
+ * [#1039](https://github.com/Shopify/hydrogen/pull/1039) [`3a297862`](https://github.com/Shopify/hydrogen/commit/3a29786202947fab0bfe876042b37a91923ed637) Thanks [@frandiox](https://github.com/frandiox)! - Update to Vite 2.9
22
+
23
+ - [#1026](https://github.com/Shopify/hydrogen/pull/1026) [`836b064d`](https://github.com/Shopify/hydrogen/commit/836b064d1648fb1a9f209a08a82ee5c20f7dfba9) Thanks [@frehner](https://github.com/frehner)! - Updated the Typescript types and GraphQL schema to the newest updates from Storefront API 2022-04. Of note in this update is the ability to skip `edges` and go directly to `node`, for example: `product.nodes[0]` instead of `product.edges[0].node`
24
+
25
+ * [#1032](https://github.com/Shopify/hydrogen/pull/1032) [`03488083`](https://github.com/Shopify/hydrogen/commit/034880833dc500f66f9b67417c00099c283dfa67) Thanks [@jplhomer](https://github.com/jplhomer)! - Catch hydration errors related to experimental server components bugs and prevent them from being logged in production.
26
+
27
+ - [#1037](https://github.com/Shopify/hydrogen/pull/1037) [`13376efb`](https://github.com/Shopify/hydrogen/commit/13376efbe4db93efd705b6900a6198708bc37e69) Thanks [@jplhomer](https://github.com/jplhomer)! - Use new header for private Storefront token
28
+
29
+ ## 0.13.2
30
+
31
+ ### Patch Changes
32
+
33
+ - [#1013](https://github.com/Shopify/hydrogen/pull/1013) [`94dc94ae`](https://github.com/Shopify/hydrogen/commit/94dc94aeb9dfd5e0120cab610203fdb4f0c61d3c) Thanks [@jplhomer](https://github.com/jplhomer)! - Fix CORS issue in StackBlitz
34
+
35
+ ## 0.13.1
36
+
37
+ ### Patch Changes
38
+
39
+ - [#1008](https://github.com/Shopify/hydrogen/pull/1008) [`ca1de82b`](https://github.com/Shopify/hydrogen/commit/ca1de82bc38c1c02caa451fb52065da499555e6f) Thanks [@frandiox](https://github.com/frandiox)! - Allow passing `cache` parameter to `createServer` in Node entry.
40
+
41
+ * [#997](https://github.com/Shopify/hydrogen/pull/997) [`fffdc08f`](https://github.com/Shopify/hydrogen/commit/fffdc08f87f71592352a2eb67a63e80704054db2) Thanks [@frandiox](https://github.com/frandiox)! - Allow empty array values in flattenConnection utility.
42
+
43
+ - [#1007](https://github.com/Shopify/hydrogen/pull/1007) [`7cfca7b0`](https://github.com/Shopify/hydrogen/commit/7cfca7b09289e028a463ababb51e69b4e3943d94) Thanks [@scottdixon](https://github.com/scottdixon)! - Fix API index routes https://github.com/Shopify/hydrogen/issues/562
44
+
45
+ * [#1000](https://github.com/Shopify/hydrogen/pull/1000) [`6d0d5068`](https://github.com/Shopify/hydrogen/commit/6d0d50686029c3d66d9dc0ceb0b5f71456c7b19e) Thanks [@frandiox](https://github.com/frandiox)! - Do not cache Storefront API responses that contain GraphQL errors.
46
+
47
+ - [#1003](https://github.com/Shopify/hydrogen/pull/1003) [`d8a9c929`](https://github.com/Shopify/hydrogen/commit/d8a9c9290aaf7c9d058b2c08567294822bea5396) Thanks [@jplhomer](https://github.com/jplhomer)! - Update useShopQuery to accept a custom Storefront API secret token, and forward the Buyer IP.
48
+
3
49
  ## 0.13.0
4
50
 
5
51
  ### Minor Changes
@@ -29,7 +75,7 @@
29
75
 
30
76
  These fragments have been removed to reduce the chances of over-fetching (in other words, querying for fields you don't use) in your GraphQL queries. Please refer to the [Storefront API documentation](https://shopify.dev/api/storefront) for information and guides.
31
77
 
32
- * [#912](https://github.com/Shopify/hydrogen/pull/912) [`de0e0d6a`](https://github.com/Shopify/hydrogen/commit/de0e0d6a6652463243ee09013cd30830ce2a246a) Thanks [@blittle](https://github.com/blittle)! - Change the country selector to lazy load available countries. The motivation to do so is that a _lot_ of countries come with the starter template. The problem is 1) the graphql query to fetch them all is relatively slow and 2) all of them get serialized to the browser in each RSC response.
78
+ * [#912](https://github.com/Shopify/hydrogen/pull/912) [`de0e0d6a`](https://github.com/Shopify/hydrogen/commit/de0e0d6a6652463243ee09013cd30830ce2a246a) Thanks [@blittle](https://github.com/blittle)! - Change the country selector to lazy load available countries. The motivation to do so is that a _lot_ of countries come with the Demo Store template. The problem is 1) the graphql query to fetch them all is relatively slow and 2) all of them get serialized to the browser in each RSC response.
33
79
 
34
80
  This change removes `availableCountries` from the `LocalizationProvider`. As a result, the `useAvailableCountries` hook is also gone. Instead, the available countries are loaded on demand from an API route.
35
81
 
@@ -79,7 +125,7 @@
79
125
  }, []);
80
126
  ```
81
127
 
82
- See an example on how this could be done inside the Hydrogen Example Template [country selector](https://github.com/Shopify/hydrogen/blob/v1.x-2022-07/examples/template-hydrogen-default/src/components/CountrySelector.client.jsx)
128
+ See an example on how this could be done inside the Demo Store template [country selector](https://github.com/Shopify/hydrogen/blob/v1.x-2022-07/examples/template-hydrogen-default/src/components/CountrySelector.client.jsx)
83
129
 
84
130
  - [#698](https://github.com/Shopify/hydrogen/pull/698) [`6f30b9a1`](https://github.com/Shopify/hydrogen/commit/6f30b9a1327f06d648a01dd94d539c7dcb3061e0) Thanks [@jplhomer](https://github.com/jplhomer)! - Basic end-to-end tests have been added to the default Hydrogen template. You can run tests in development:
85
131
 
@@ -239,7 +285,7 @@
239
285
 
240
286
  - [#981](https://github.com/Shopify/hydrogen/pull/981) [`8dda8a86`](https://github.com/Shopify/hydrogen/commit/8dda8a860bc1cf58511756b6fff999fb7caa6081) Thanks [@michenly](https://github.com/michenly)! - Fix useUrl() when it is in RSC mode
241
287
 
242
- * [#965](https://github.com/Shopify/hydrogen/pull/965) [`cdad13ed`](https://github.com/Shopify/hydrogen/commit/cdad13ed85ff17b84981367f39c7d2fe45e72dcf) Thanks [@blittle](https://github.com/blittle)! - Fix server redirects to work properly with RSC responses. For example, the redirect component within the starter template needs to change:
288
+ * [#965](https://github.com/Shopify/hydrogen/pull/965) [`cdad13ed`](https://github.com/Shopify/hydrogen/commit/cdad13ed85ff17b84981367f39c7d2fe45e72dcf) Thanks [@blittle](https://github.com/blittle)! - Fix server redirects to work properly with RSC responses. For example, the redirect component within the Demo Store template needs to change:
243
289
 
244
290
  ```diff
245
291
  export default function Redirect({response}) {
@@ -289,7 +335,7 @@
289
335
 
290
336
  ### Minor Changes
291
337
 
292
- - [`8271be8`](https://github.com/Shopify/hydrogen/commit/8271be83331c99f27a258e6532983da4fe4f0b5b) Thanks [@michenly](https://github.com/michenly)! - Export Seo components Fragement and use them in the starter template.
338
+ - [`8271be8`](https://github.com/Shopify/hydrogen/commit/8271be83331c99f27a258e6532983da4fe4f0b5b) Thanks [@michenly](https://github.com/michenly)! - Export Seo components Fragement and use them in the Demo Store template.
293
339
 
294
340
  * [#827](https://github.com/Shopify/hydrogen/pull/827) [`745e8c0`](https://github.com/Shopify/hydrogen/commit/745e8c0a87a7c41803934565e5a756295ff629c2) Thanks [@michenly](https://github.com/michenly)! - Move any static `Fragment` properties on components to the entry point `@shopify/hydrogen/fragments`.
295
341
  The migration diff are as follows:
@@ -925,7 +971,7 @@ function SomeComponent() {
925
971
 
926
972
  ### Fixed
927
973
 
928
- - Starter template GalleryPreview unique key warning
974
+ - Demo Store template GalleryPreview unique key warning
929
975
  - Mitigation for upcoming breaking minor Vite update
930
976
 
931
977
  ## 0.2.0 - 2021-10-08
@@ -8,6 +8,8 @@ export interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorEle
8
8
  clientState?: any;
9
9
  /** Whether to reload the whole document on navigation. */
10
10
  reloadDocument?: boolean;
11
+ /** Whether to prefetch the link source when the user signals intent. Defaults to `true`. For more information, refer to [Prefetching a link source](/custom-storefronts/hydrogen/framework/routes#prefetching-a-link-source). */
12
+ prefetch?: boolean;
11
13
  }
12
14
  /**
13
15
  * The `Link` component is used to navigate between routes. Because it renders an underlying `<a>` element, all
@@ -15,3 +17,7 @@ export interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorEle
15
17
  * For more information, refer to the [`<a>` element documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attributes).
16
18
  */
17
19
  export declare const Link: React.ForwardRefExoticComponent<LinkProps & React.RefAttributes<HTMLAnchorElement>>;
20
+ /**
21
+ * Credit: Remix's <Link> component.
22
+ */
23
+ export declare function composeEventHandlers<EventType extends React.SyntheticEvent | Event>(theirHandler: ((event: EventType) => any) | undefined, ourHandler: (event: EventType) => any): (event: EventType) => any;
@@ -1,7 +1,9 @@
1
- import React, { useCallback } from 'react';
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
2
  import { useRouter } from '../../foundation/Router/BrowserRouter.client';
3
3
  import { createPath } from 'history';
4
4
  import { useNavigate } from '../../foundation/useNavigate/useNavigate';
5
+ import { useServerState } from '../../foundation/useServerState';
6
+ import { RSC_PATHNAME } from '../../constants';
5
7
  /**
6
8
  * The `Link` component is used to navigate between routes. Because it renders an underlying `<a>` element, all
7
9
  * properties available to the `<a>` element are also available to the `Link` component.
@@ -10,7 +12,13 @@ import { useNavigate } from '../../foundation/useNavigate/useNavigate';
10
12
  export const Link = React.forwardRef(function Link(props, ref) {
11
13
  const navigate = useNavigate();
12
14
  const { location } = useRouter();
13
- const { reloadDocument, target, replace: _replace, to, onClick, clientState, } = props;
15
+ const [_, startTransition] = React.useTransition();
16
+ /**
17
+ * Inspired by Remix's Link component
18
+ */
19
+ const [shouldPrefetch, setShouldPrefetch] = useState(false);
20
+ const [maybePrefetch, setMaybePrefetch] = useState(false);
21
+ const { reloadDocument, target, replace: _replace, to, onClick, clientState, prefetch = true, } = props;
14
22
  const internalClick = useCallback((e) => {
15
23
  if (onClick)
16
24
  onClick(e);
@@ -28,8 +36,82 @@ export const Link = React.forwardRef(function Link(props, ref) {
28
36
  });
29
37
  }
30
38
  }, [reloadDocument, target, _replace, to, clientState, onClick, location]);
31
- return (React.createElement("a", { ...without(props, ['to', 'replace', 'clientState', 'reloadDocument']), ref: ref, onClick: internalClick, href: props.to }, props.children));
39
+ const signalPrefetchIntent = () => {
40
+ /**
41
+ * startTransition to yield to more important updates
42
+ */
43
+ startTransition(() => {
44
+ if (prefetch) {
45
+ setMaybePrefetch(true);
46
+ }
47
+ });
48
+ };
49
+ const cancelPrefetchIntent = () => {
50
+ /**
51
+ * startTransition to yield to more important updates
52
+ */
53
+ startTransition(() => {
54
+ if (prefetch) {
55
+ setMaybePrefetch(false);
56
+ }
57
+ });
58
+ };
59
+ /**
60
+ * Wrapping `maybePrefetch` inside useEffect allows the user to quickly graze over
61
+ * a link without triggering a prefetch.
62
+ */
63
+ useEffect(() => {
64
+ if (maybePrefetch) {
65
+ const id = setTimeout(() => {
66
+ setShouldPrefetch(true);
67
+ }, 100);
68
+ return () => {
69
+ clearTimeout(id);
70
+ };
71
+ }
72
+ }, [maybePrefetch]);
73
+ const onMouseEnter = composeEventHandlers(props.onMouseEnter, signalPrefetchIntent);
74
+ const onMouseLeave = composeEventHandlers(props.onMouseLeave, cancelPrefetchIntent);
75
+ const onFocus = composeEventHandlers(props.onFocus, signalPrefetchIntent);
76
+ const onBlur = composeEventHandlers(props.onBlur, cancelPrefetchIntent);
77
+ const onTouchStart = composeEventHandlers(props.onTouchStart, signalPrefetchIntent);
78
+ return (React.createElement(React.Fragment, null,
79
+ React.createElement("a", { ...without(props, [
80
+ 'to',
81
+ 'replace',
82
+ 'clientState',
83
+ 'reloadDocument',
84
+ 'prefetch',
85
+ ]), ref: ref, onClick: internalClick, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, onFocus: onFocus, onBlur: onBlur, onTouchStart: onTouchStart, href: props.to }, props.children),
86
+ shouldPrefetch && React.createElement(Prefetch, { pathname: to })));
32
87
  });
88
+ function Prefetch({ pathname }) {
89
+ const { getProposedServerState } = useServerState();
90
+ const { location } = useRouter();
91
+ const newPath = createPath({ pathname });
92
+ if (pathname.startsWith('http') || newPath === createPath(location)) {
93
+ return null;
94
+ }
95
+ const newLocation = new URL(newPath, window.location.href);
96
+ const proposedServerState = getProposedServerState({
97
+ pathname: newLocation.pathname,
98
+ search: newLocation.search || undefined,
99
+ });
100
+ const href = `${RSC_PATHNAME}?state=` +
101
+ encodeURIComponent(JSON.stringify(proposedServerState));
102
+ return React.createElement("link", { rel: "prefetch", as: "fetch", href: href });
103
+ }
104
+ /**
105
+ * Credit: Remix's <Link> component.
106
+ */
107
+ export function composeEventHandlers(theirHandler, ourHandler) {
108
+ return (event) => {
109
+ theirHandler === null || theirHandler === void 0 ? void 0 : theirHandler(event);
110
+ if (!event.defaultPrevented) {
111
+ ourHandler(event);
112
+ }
113
+ };
114
+ }
33
115
  function isModifiedEvent(event) {
34
116
  return !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
35
117
  }
@@ -1,4 +1,4 @@
1
- import { LocalizationQuery } from './LocalizationQuery';
1
+ import type { LocalizationQuery } from './LocalizationProvider.server';
2
2
  export declare type Localization = LocalizationQuery['localization'];
3
3
  export interface LocalizationContextValue {
4
4
  country?: Localization['country'];
@@ -1,5 +1,6 @@
1
1
  import { ReactNode } from 'react';
2
2
  import { PreloadOptions } from '../../types';
3
+ import { Country, Currency } from '../../storefront-api-types';
3
4
  export interface LocalizationProviderProps {
4
5
  /** A `ReactNode` element. */
5
6
  children: ReactNode;
@@ -19,3 +20,18 @@ export interface LocalizationProviderProps {
19
20
  * `@inContext` directive as the `country` value.
20
21
  */
21
22
  export declare function LocalizationProvider(props: LocalizationProviderProps): JSX.Element;
23
+ export declare type LocalizationQuery = {
24
+ __typename?: 'QueryRoot';
25
+ } & {
26
+ localization: {
27
+ __typename?: 'Localization';
28
+ } & {
29
+ country: {
30
+ __typename?: 'Country';
31
+ } & Pick<Country, 'isoCode' | 'name'> & {
32
+ currency: {
33
+ __typename?: 'Currency';
34
+ } & Pick<Currency, 'isoCode'>;
35
+ };
36
+ };
37
+ };
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import LocalizationClientProvider from './LocalizationClientProvider.client';
3
+ import { useShop } from '../../foundation/useShop';
3
4
  import { useShopQuery } from '../../hooks/useShopQuery';
4
5
  import { CacheDays } from '../../framework/CachingStrategy';
5
6
  /**
@@ -12,15 +13,19 @@ import { CacheDays } from '../../framework/CachingStrategy';
12
13
  * `@inContext` directive as the `country` value.
13
14
  */
14
15
  export function LocalizationProvider(props) {
16
+ const { languageCode } = useShop();
15
17
  const { data: { localization }, } = useShopQuery({
16
18
  query: query,
19
+ variables: { language: languageCode },
17
20
  cache: CacheDays(),
18
21
  preload: props.preload,
19
22
  });
20
23
  return (React.createElement(LocalizationClientProvider, { localization: localization }, props.children));
21
24
  }
22
- const query = `query Localization {
23
- localization {
25
+ const query = `
26
+ query Localization($language: LanguageCode)
27
+ @inContext(language: $language) {
28
+ localization {
24
29
  country {
25
30
  isoCode
26
31
  name
@@ -5,8 +5,7 @@ import { TitleSeo } from './TitleSeo.client';
5
5
  import { DescriptionSeo } from './DescriptionSeo.client';
6
6
  import { TwitterSeo } from './TwitterSeo.client';
7
7
  export function DefaultPageSeo({ title, description, url, titleTemplate, lang, }) {
8
- const { locale } = useShop();
9
- const fallBacklang = locale.split(/[-_]/)[0];
8
+ const { languageCode: fallBacklang } = useShop();
10
9
  return (React.createElement(React.Fragment, null,
11
10
  React.createElement(Head, { defaultTitle: title !== null && title !== void 0 ? title : '', titleTemplate: titleTemplate !== null && titleTemplate !== void 0 ? titleTemplate : `%s - ${title}` },
12
11
  React.createElement("html", { lang: lang !== null && lang !== void 0 ? lang : fallBacklang }),
@@ -19,10 +19,23 @@ const renderHydrogen = async (ClientWrapper, config) => {
19
19
  }
20
20
  // default to StrictMode on, unless explicitly turned off
21
21
  const RootComponent = (config === null || config === void 0 ? void 0 : config.strictMode) !== false ? StrictMode : Fragment;
22
+ let hasCaughtError = false;
22
23
  hydrateRoot(root, React.createElement(RootComponent, null,
23
24
  React.createElement(ErrorBoundary, { FallbackComponent: Error },
24
25
  React.createElement(Suspense, { fallback: null },
25
- React.createElement(Content, { clientWrapper: ClientWrapper })))));
26
+ React.createElement(Content, { clientWrapper: ClientWrapper })))), {
27
+ onRecoverableError(e) {
28
+ if (__DEV__ && !hasCaughtError) {
29
+ hasCaughtError = true;
30
+ console.log(`React encountered an error while attempting to hydrate the application. ` +
31
+ `This is likely due to a bug in React's Suspense behavior related to experimental server components, ` +
32
+ `and it is safe to ignore this error.\n` +
33
+ `Visit this issue to learn more: https://github.com/Shopify/hydrogen/issues/920.\n\n` +
34
+ `The original error is printed below:`);
35
+ console.log(e);
36
+ }
37
+ },
38
+ });
26
39
  };
27
40
  export default renderHydrogen;
28
41
  function Content({ clientWrapper: ClientWrapper = ({ children }) => children, }) {
@@ -14,6 +14,7 @@ interface RequestHandlerOptions {
14
14
  dev?: boolean;
15
15
  context?: RuntimeContext;
16
16
  nonce?: string;
17
+ buyerIpHeader?: string;
17
18
  }
18
19
  export interface RequestHandler {
19
20
  (request: Request | IncomingMessage, options: RequestHandlerOptions): Promise<Response | undefined>;
@@ -5,7 +5,6 @@ import { defer } from './utilities/defer';
5
5
  import { Html, applyHtmlHead } from './framework/Hydration/Html';
6
6
  import { ServerComponentResponse } from './framework/Hydration/ServerComponentResponse.server';
7
7
  import { ServerComponentRequest } from './framework/Hydration/ServerComponentRequest.server';
8
- import { getCacheControlHeader } from './framework/cache';
9
8
  import { preloadRequestCacheData, ServerRequestProvider, } from './foundation/ServerRequestProvider';
10
9
  import { getApiRouteFromURL, renderApiRoute, getApiRoutes, } from './utilities/apiRoutes';
11
10
  import { ServerStateProvider } from './foundation/ServerStateProvider';
@@ -19,8 +18,9 @@ const DOCTYPE = '<!DOCTYPE html>';
19
18
  const CONTENT_TYPE = 'Content-Type';
20
19
  const HTML_CONTENT_TYPE = 'text/html; charset=UTF-8';
21
20
  export const renderHydrogen = (App, { shopifyConfig, routes }) => {
22
- const handleRequest = async function (rawRequest, { indexTemplate, streamableResponse, dev, cache, context, nonce }) {
21
+ const handleRequest = async function (rawRequest, { indexTemplate, streamableResponse, dev, cache, context, nonce, buyerIpHeader, }) {
23
22
  const request = new ServerComponentRequest(rawRequest);
23
+ request.ctx.buyerIpHeader = buyerIpHeader;
24
24
  const url = new URL(request.url);
25
25
  const log = getLoggerWithContext(request);
26
26
  const componentResponse = new ServerComponentResponse();
@@ -108,7 +108,7 @@ async function render(url, { App, routes, request, componentResponse, log, templ
108
108
  * TODO: Also add `Vary` headers for `accept-language` and any other keys
109
109
  * we want to shard our full-page cache for all Hydrogen storefronts.
110
110
  */
111
- headers[getCacheControlHeader({ dev })] = componentResponse.cacheControlHeader;
111
+ headers['cache-control'] = componentResponse.cacheControlHeader;
112
112
  if (componentResponse.customBody) {
113
113
  // This can be used to return sitemap.xml or any other custom response.
114
114
  postRequestTasks('ssr', status, request, componentResponse);
@@ -203,7 +203,7 @@ async function stream(url, { App, routes, request, response, componentResponse,
203
203
  * queries which might be caught behind Suspense. Clarify this or add
204
204
  * additional checks downstream?
205
205
  */
206
- responseOptions.headers[getCacheControlHeader({ dev })] =
206
+ responseOptions.headers['cache-control'] =
207
207
  componentResponse.cacheControlHeader;
208
208
  if (isRedirect(responseOptions)) {
209
209
  return false;
@@ -271,7 +271,7 @@ async function stream(url, { App, routes, request, response, componentResponse,
271
271
  * queries which might be caught behind Suspense. Clarify this or add
272
272
  * additional checks downstream?
273
273
  */
274
- response.setHeader(getCacheControlHeader({ dev }), componentResponse.cacheControlHeader);
274
+ response.setHeader('cache-control', componentResponse.cacheControlHeader);
275
275
  writeHeadToServerResponse(response, componentResponse, log, didError);
276
276
  if (isRedirect(response)) {
277
277
  // Return redirects early without further rendering/streaming
@@ -355,15 +355,21 @@ async function hydrate(url, { App, routes, request, response, componentResponse,
355
355
  // Note: CFW does not support reader.piteTo nor iterable syntax
356
356
  const bufferedBody = await bufferReadableStream(rscReadable.getReader());
357
357
  postRequestTasks('rsc', 200, request, componentResponse);
358
- return new Response(bufferedBody);
358
+ return new Response(bufferedBody, {
359
+ headers: {
360
+ 'cache-control': componentResponse.cacheControlHeader,
361
+ },
362
+ });
359
363
  }
360
364
  else if (response) {
361
365
  const rscWriter = await import(
362
366
  // @ts-ignore
363
367
  '@shopify/hydrogen/vendor/react-server-dom-vite/writer.node.server');
364
- const stream = rscWriter
365
- .renderToPipeableStream(AppRSC)
366
- .pipe(response);
368
+ const streamer = rscWriter.renderToPipeableStream(AppRSC);
369
+ response.writeHead(200, 'ok', {
370
+ 'cache-control': componentResponse.cacheControlHeader,
371
+ });
372
+ const stream = streamer.pipe(response);
367
373
  stream.on('finish', function () {
368
374
  postRequestTasks('rsc', response.statusCode, request, componentResponse);
369
375
  });
@@ -30,7 +30,7 @@ export function useServerRequest() {
30
30
  const cache = React.unstable_getCacheForType(requestCacheRSC);
31
31
  request = cache ? cache.get(requestCacheRSC.key) : null;
32
32
  }
33
- catch (error) {
33
+ catch (_a) {
34
34
  // If RSC cache failed it means this is not an RSC request.
35
35
  // Try getting SSR context instead:
36
36
  request = useContext(RequestContextSSR);
@@ -7,13 +7,18 @@ export interface ServerState {
7
7
  search: string;
8
8
  [key: string]: any;
9
9
  }
10
+ declare type ServerStateSetterInput = ((prev: ServerState) => Partial<ServerState>) | Partial<ServerState> | string;
10
11
  export interface ServerStateSetter {
11
- (input: ((prev: ServerState) => Partial<ServerState>) | Partial<ServerState> | string, propValue?: any): void;
12
+ (input: ServerStateSetterInput, propValue?: any): void;
13
+ }
14
+ interface ProposedServerStateSetter {
15
+ (input: ServerStateSetterInput, propValue?: any): ServerState;
12
16
  }
13
17
  export interface ServerStateContextValue {
14
18
  pending: boolean;
15
19
  serverState: ServerState;
16
20
  setServerState: ServerStateSetter;
21
+ getProposedServerState: ProposedServerStateSetter;
17
22
  }
18
23
  export declare const ServerStateContext: React.Context<ServerStateContextValue>;
19
24
  interface ServerStateProviderProps {
@@ -14,34 +14,39 @@ export function ServerStateProvider({ serverState, setServerState, children, })
14
14
  * the `pending` flag also provided by the hook to display in the UI.
15
15
  */
16
16
  startTransition(() => {
17
- return setServerState((prev) => {
18
- let newValue;
19
- if (typeof input === 'function') {
20
- newValue = input(prev);
21
- }
22
- else if (typeof input === 'string') {
23
- newValue = { [input]: propValue };
24
- }
25
- else {
26
- newValue = input;
27
- }
28
- if (__DEV__) {
29
- const privateProp = PRIVATE_PROPS.find((prop) => prop in newValue);
30
- if (privateProp) {
31
- console.warn(`Custom "${privateProp}" property in server state is ignored. Use a different name.`);
32
- }
33
- }
34
- return {
35
- ...prev,
36
- ...newValue,
37
- };
38
- });
17
+ return setServerState((prev) => getNewServerState(prev, input, propValue));
39
18
  });
40
19
  }, [setServerState, startTransition]);
20
+ const getProposedServerStateCallback = useCallback((input, propValue) => {
21
+ return getNewServerState(serverState, input, propValue);
22
+ }, [serverState]);
23
+ function getNewServerState(prev, input, propValue) {
24
+ let newValue;
25
+ if (typeof input === 'function') {
26
+ newValue = input(prev);
27
+ }
28
+ else if (typeof input === 'string') {
29
+ newValue = { [input]: propValue };
30
+ }
31
+ else {
32
+ newValue = input;
33
+ }
34
+ if (__DEV__) {
35
+ const privateProp = PRIVATE_PROPS.find((prop) => prop in newValue);
36
+ if (privateProp) {
37
+ console.warn(`Custom "${privateProp}" property in server state is ignored. Use a different name.`);
38
+ }
39
+ }
40
+ return {
41
+ ...prev,
42
+ ...newValue,
43
+ };
44
+ }
41
45
  const value = useMemo(() => ({
42
46
  pending,
43
47
  serverState,
44
48
  setServerState: setServerStateCallback,
49
+ getProposedServerState: getProposedServerStateCallback,
45
50
  }), [serverState, setServerStateCallback, pending]);
46
51
  return (React.createElement(ServerStateContext.Provider, { value: value }, children));
47
52
  }
@@ -4,8 +4,11 @@ import { DEFAULT_LOCALE } from '../constants';
4
4
  import { useServerRequest } from '../ServerRequestProvider';
5
5
  function makeShopifyContext(shopifyConfig) {
6
6
  var _a, _b;
7
+ const locale = (_a = shopifyConfig.defaultLocale) !== null && _a !== void 0 ? _a : DEFAULT_LOCALE;
8
+ const languageCode = locale.split(/[-_]/)[0];
7
9
  return {
8
- locale: (_a = shopifyConfig.defaultLocale) !== null && _a !== void 0 ? _a : DEFAULT_LOCALE,
10
+ locale: locale.toUpperCase(),
11
+ languageCode: languageCode.toUpperCase(),
9
12
  storeDomain: (_b = shopifyConfig === null || shopifyConfig === void 0 ? void 0 : shopifyConfig.storeDomain) === null || _b === void 0 ? void 0 : _b.replace(/^https?:\/\//, ''),
10
13
  storefrontToken: shopifyConfig.storefrontToken,
11
14
  storefrontApiVersion: shopifyConfig.storefrontApiVersion,
@@ -1,7 +1,9 @@
1
+ import type { CountryCode, LanguageCode } from '../../storefront-api-types';
1
2
  import type { ReactNode } from 'react';
2
3
  import type { ShopifyConfig } from '../../types';
3
4
  export declare type ShopifyContextValue = {
4
- locale: string;
5
+ locale: `${LanguageCode}-${CountryCode}`;
6
+ languageCode: `${LanguageCode}`;
5
7
  storeDomain: ShopifyConfig['storeDomain'];
6
8
  storefrontToken: ShopifyConfig['storefrontToken'];
7
9
  storefrontApiVersion: string;
@@ -1 +1 @@
1
- export declare const DEFAULT_LOCALE = "en-us";
1
+ export declare const DEFAULT_LOCALE = "EN-US";
@@ -1,3 +1,3 @@
1
1
  // Note: do not mix this export with other app-only logic
2
2
  // to avoid importing unnecessary code in the plugins.
3
- export const DEFAULT_LOCALE = 'en-us';
3
+ export const DEFAULT_LOCALE = 'EN-US';
@@ -9,6 +9,9 @@ export interface HydrogenUseQueryOptions {
9
9
  * to preload the query for all requests.
10
10
  */
11
11
  preload?: PreloadOptions;
12
+ /** A function that inspects the response body to determine if it should be cached.
13
+ */
14
+ shouldCacheResponse?: (body: any) => boolean;
12
15
  }
13
16
  /**
14
17
  * The `useQuery` hook executes an asynchronous operation like `fetch` in a way that
@@ -32,9 +32,11 @@ queryOptions) {
32
32
  return useRequestCacheData(withCacheIdKey, fetcher);
33
33
  }
34
34
  function cachedQueryFnBuilder(key, queryFn, queryOptions) {
35
+ var _a;
35
36
  const resolvedQueryOptions = {
36
37
  ...(queryOptions !== null && queryOptions !== void 0 ? queryOptions : {}),
37
38
  };
39
+ const shouldCacheResponse = (_a = queryOptions === null || queryOptions === void 0 ? void 0 : queryOptions.shouldCacheResponse) !== null && _a !== void 0 ? _a : (() => true);
38
40
  /**
39
41
  * Attempt to read the query from cache. If it doesn't exist or if it's stale, regenerate it.
40
42
  */
@@ -64,7 +66,9 @@ function cachedQueryFnBuilder(key, queryFn, queryOptions) {
64
66
  await setItemInCache(lockKey, true);
65
67
  try {
66
68
  const output = await generateNewOutput();
67
- await setItemInCache(key, output, resolvedQueryOptions === null || resolvedQueryOptions === void 0 ? void 0 : resolvedQueryOptions.cache);
69
+ if (shouldCacheResponse(output)) {
70
+ await setItemInCache(key, output, resolvedQueryOptions === null || resolvedQueryOptions === void 0 ? void 0 : resolvedQueryOptions.cache);
71
+ }
68
72
  }
69
73
  catch (e) {
70
74
  log.error(`Error generating async response: ${e.message}`);
@@ -80,7 +84,9 @@ function cachedQueryFnBuilder(key, queryFn, queryOptions) {
80
84
  /**
81
85
  * Important: Do this async
82
86
  */
83
- runDelayedFunction(async () => await setItemInCache(key, newOutput, resolvedQueryOptions === null || resolvedQueryOptions === void 0 ? void 0 : resolvedQueryOptions.cache));
87
+ if (shouldCacheResponse(newOutput)) {
88
+ runDelayedFunction(() => setItemInCache(key, newOutput, resolvedQueryOptions === null || resolvedQueryOptions === void 0 ? void 0 : resolvedQueryOptions.cache));
89
+ }
84
90
  collectQueryCacheControlHeaders(request, key, generateSubRequestCacheControlHeader(resolvedQueryOptions === null || resolvedQueryOptions === void 0 ? void 0 : resolvedQueryOptions.cache));
85
91
  return newOutput;
86
92
  }
@@ -35,6 +35,7 @@ export declare class ServerComponentRequest extends Request {
35
35
  queryTimings: Array<QueryTiming>;
36
36
  preloadQueries: PreloadQueriesByURL;
37
37
  router: RouterContextData;
38
+ buyerIpHeader?: string;
38
39
  [key: string]: any;
39
40
  };
40
41
  constructor(input: any);
@@ -44,4 +45,10 @@ export declare class ServerComponentRequest extends Request {
44
45
  savePreloadQuery(query: PreloadQueryEntry): void;
45
46
  getPreloadQueries(): PreloadQueriesByURL | undefined;
46
47
  savePreloadQueries(): void;
48
+ /**
49
+ * Buyer IP varies by hosting provider and runtime. The developer should provide this
50
+ * as an argument to the `handleRequest` function for their runtime.
51
+ * Defaults to `x-forwarded-for` header value.
52
+ */
53
+ getBuyerIp(): string | null;
47
54
  }