@shopify/hydrogen 0.13.2 → 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 (39) hide show
  1. package/CHANGELOG.md +26 -0
  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.js +13 -8
  10. package/dist/esnext/foundation/ServerStateProvider/ServerStateProvider.d.ts +6 -1
  11. package/dist/esnext/foundation/ServerStateProvider/ServerStateProvider.js +27 -22
  12. package/dist/esnext/foundation/ShopifyProvider/ShopifyProvider.server.js +4 -1
  13. package/dist/esnext/foundation/ShopifyProvider/types.d.ts +3 -1
  14. package/dist/esnext/foundation/constants.d.ts +1 -1
  15. package/dist/esnext/foundation/constants.js +1 -1
  16. package/dist/esnext/framework/Hydration/ServerComponentRequest.server.js +3 -2
  17. package/dist/esnext/framework/cache.d.ts +0 -8
  18. package/dist/esnext/framework/cache.js +0 -8
  19. package/dist/esnext/hooks/useParsedMetafields/useParsedMetafields.d.ts +1 -1
  20. package/dist/esnext/hooks/useShopQuery/hooks.d.ts +1 -1
  21. package/dist/esnext/hooks/useShopQuery/hooks.js +17 -9
  22. package/dist/esnext/storefront-api-types.d.ts +150 -3
  23. package/dist/esnext/storefront-api-types.js +16 -0
  24. package/dist/esnext/version.d.ts +1 -1
  25. package/dist/esnext/version.js +1 -1
  26. package/dist/node/entry-server.js +13 -8
  27. package/dist/node/foundation/ServerStateProvider/ServerStateProvider.d.ts +6 -1
  28. package/dist/node/foundation/ServerStateProvider/ServerStateProvider.js +27 -22
  29. package/dist/node/foundation/ShopifyProvider/types.d.ts +3 -1
  30. package/dist/node/framework/Hydration/ServerComponentRequest.server.js +3 -2
  31. package/dist/node/framework/cache.d.ts +0 -8
  32. package/dist/node/framework/cache.js +1 -10
  33. package/dist/node/storefront-api-types.d.ts +150 -3
  34. package/dist/node/storefront-api-types.js +17 -1
  35. package/dist/node/version.d.ts +1 -1
  36. package/dist/node/version.js +1 -1
  37. package/package.json +3 -3
  38. package/dist/esnext/components/LocalizationProvider/LocalizationQuery.d.ts +0 -23
  39. package/dist/esnext/components/LocalizationProvider/LocalizationQuery.js +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
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
+
3
29
  ## 0.13.2
4
30
 
5
31
  ### Patch Changes
@@ -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, }) {
@@ -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';
@@ -109,7 +108,7 @@ async function render(url, { App, routes, request, componentResponse, log, templ
109
108
  * TODO: Also add `Vary` headers for `accept-language` and any other keys
110
109
  * we want to shard our full-page cache for all Hydrogen storefronts.
111
110
  */
112
- headers[getCacheControlHeader({ dev })] = componentResponse.cacheControlHeader;
111
+ headers['cache-control'] = componentResponse.cacheControlHeader;
113
112
  if (componentResponse.customBody) {
114
113
  // This can be used to return sitemap.xml or any other custom response.
115
114
  postRequestTasks('ssr', status, request, componentResponse);
@@ -204,7 +203,7 @@ async function stream(url, { App, routes, request, response, componentResponse,
204
203
  * queries which might be caught behind Suspense. Clarify this or add
205
204
  * additional checks downstream?
206
205
  */
207
- responseOptions.headers[getCacheControlHeader({ dev })] =
206
+ responseOptions.headers['cache-control'] =
208
207
  componentResponse.cacheControlHeader;
209
208
  if (isRedirect(responseOptions)) {
210
209
  return false;
@@ -272,7 +271,7 @@ async function stream(url, { App, routes, request, response, componentResponse,
272
271
  * queries which might be caught behind Suspense. Clarify this or add
273
272
  * additional checks downstream?
274
273
  */
275
- response.setHeader(getCacheControlHeader({ dev }), componentResponse.cacheControlHeader);
274
+ response.setHeader('cache-control', componentResponse.cacheControlHeader);
276
275
  writeHeadToServerResponse(response, componentResponse, log, didError);
277
276
  if (isRedirect(response)) {
278
277
  // Return redirects early without further rendering/streaming
@@ -356,15 +355,21 @@ async function hydrate(url, { App, routes, request, response, componentResponse,
356
355
  // Note: CFW does not support reader.piteTo nor iterable syntax
357
356
  const bufferedBody = await bufferReadableStream(rscReadable.getReader());
358
357
  postRequestTasks('rsc', 200, request, componentResponse);
359
- return new Response(bufferedBody);
358
+ return new Response(bufferedBody, {
359
+ headers: {
360
+ 'cache-control': componentResponse.cacheControlHeader,
361
+ },
362
+ });
360
363
  }
361
364
  else if (response) {
362
365
  const rscWriter = await import(
363
366
  // @ts-ignore
364
367
  '@shopify/hydrogen/vendor/react-server-dom-vite/writer.node.server');
365
- const stream = rscWriter
366
- .renderToPipeableStream(AppRSC)
367
- .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);
368
373
  stream.on('finish', function () {
369
374
  postRequestTasks('rsc', response.statusCode, request, componentResponse);
370
375
  });
@@ -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';
@@ -123,8 +123,9 @@ function getInitFromNodeRequest(request) {
123
123
  ? request.body
124
124
  : undefined,
125
125
  };
126
- if (!init.headers.has('x-forwarded-for')) {
127
- init.headers.set('x-forwarded-for', request.socket.remoteAddress);
126
+ const remoteAddress = request.socket.remoteAddress;
127
+ if (!init.headers.has('x-forwarded-for') && remoteAddress) {
128
+ init.headers.set('x-forwarded-for', remoteAddress);
128
129
  }
129
130
  return init;
130
131
  }
@@ -1,13 +1,5 @@
1
1
  import type { QueryKey, CachingStrategy } from '../types';
2
2
  export declare function generateSubRequestCacheControlHeader(userCacheOptions?: CachingStrategy): string;
3
- /**
4
- * Use a preview header during development.
5
- * TODO: Support an override of this to force the cache
6
- * header to be present during dev. ENV var maybe?
7
- */
8
- export declare function getCacheControlHeader({ dev }: {
9
- dev?: boolean;
10
- }): "cache-control-preview" | "cache-control";
11
3
  export declare function hashKey(key: QueryKey): string;
12
4
  /**
13
5
  * Get an item from the cache. If a match is found, returns a tuple
@@ -3,14 +3,6 @@ import { CacheSeconds, generateCacheControlHeader, } from '../framework/CachingS
3
3
  export function generateSubRequestCacheControlHeader(userCacheOptions) {
4
4
  return generateCacheControlHeader(userCacheOptions || CacheSeconds());
5
5
  }
6
- /**
7
- * Use a preview header during development.
8
- * TODO: Support an override of this to force the cache
9
- * header to be present during dev. ENV var maybe?
10
- */
11
- export function getCacheControlHeader({ dev }) {
12
- return dev ? 'cache-control-preview' : 'cache-control';
13
- }
14
6
  export function hashKey(key) {
15
7
  const rawKey = key instanceof Array ? key : [key];
16
8
  /**
@@ -15,7 +15,7 @@ metafields?: PartialDeep<MetafieldConnection>): {
15
15
  key?: string | undefined;
16
16
  namespace?: string | undefined;
17
17
  parentResource?: import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").ProductVariant> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Product> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Blog> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Article> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Customer> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Order> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Collection> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Page> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Shop> | undefined;
18
- reference?: import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").MediaImage> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").ProductVariant> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Product> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Page> | null | undefined;
18
+ reference?: import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Video> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").MediaImage> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").ProductVariant> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Product> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").Page> | import("type-fest/source/partial-deep").PartialObjectDeep<import("../../storefront-api-types").GenericFile> | null | undefined;
19
19
  type?: string | undefined;
20
20
  updatedAt?: string | undefined;
21
21
  }[];
@@ -8,7 +8,7 @@ export interface UseShopQueryResponse<T> {
8
8
  /**
9
9
  * The `useShopQuery` hook allows you to make server-only GraphQL queries to the Storefront API. It must be a descendent of a `ShopifyProvider` component.
10
10
  */
11
- export declare function useShopQuery<T>({ query, variables, cache, locale, preload, }: {
11
+ export declare function useShopQuery<T>({ query, variables, cache, preload, }: {
12
12
  /** A string of the GraphQL query.
13
13
  * If no query is provided, useShopQuery will make no calls to the Storefront API.
14
14
  */
@@ -8,11 +8,12 @@ import { injectGraphQLTracker } from '../../utilities/graphql-tracker';
8
8
  import { sendMessageToClient } from '../../utilities/devtools';
9
9
  import { META_ENV_SSR } from '../../foundation/ssr-interop';
10
10
  // Check if the response body has GraphQL errors
11
- const shouldCacheResponse = (body) => { var _a; return !((body === null || body === void 0 ? void 0 : body.error) || ((_a = body === null || body === void 0 ? void 0 : body.data) === null || _a === void 0 ? void 0 : _a.errors)); };
11
+ // https://spec.graphql.org/June2018/#sec-Response-Format
12
+ const shouldCacheResponse = (body) => !(body === null || body === void 0 ? void 0 : body.errors);
12
13
  /**
13
14
  * The `useShopQuery` hook allows you to make server-only GraphQL queries to the Storefront API. It must be a descendent of a `ShopifyProvider` component.
14
15
  */
15
- export function useShopQuery({ query, variables = {}, cache, locale = '', preload = false, }) {
16
+ export function useShopQuery({ query, variables = {}, cache, preload = false, }) {
16
17
  var _a;
17
18
  if (!META_ENV_SSR) {
18
19
  throw new Error('Shopify Storefront API requests should only be made from the server.');
@@ -20,7 +21,7 @@ export function useShopQuery({ query, variables = {}, cache, locale = '', preloa
20
21
  const serverRequest = useServerRequest();
21
22
  const log = getLoggerWithContext(serverRequest);
22
23
  const body = query ? graphqlRequestBody(query, variables) : '';
23
- const { key, url, requestInit } = useCreateShopRequest(body, locale);
24
+ const { key, url, requestInit } = useCreateShopRequest(body);
24
25
  const { data, error: useQueryError } = useQuery(key, query
25
26
  ? fetchBuilder(url, requestInit)
26
27
  : // If no query, avoid calling SFAPI & return nothing
@@ -93,30 +94,37 @@ export function useShopQuery({ query, variables = {}, cache, locale = '', preloa
93
94
  }
94
95
  return data;
95
96
  }
96
- function useCreateShopRequest(body, locale) {
97
- var _a, _b;
98
- const { storeDomain, storefrontToken, storefrontApiVersion, locale: defaultLocale, } = useShop();
97
+ function useCreateShopRequest(body) {
98
+ var _a;
99
+ const { storeDomain, storefrontToken, storefrontApiVersion } = useShop();
99
100
  const request = useServerRequest();
100
101
  const secretToken = typeof Oxygen !== 'undefined'
101
102
  ? (_a = Oxygen === null || Oxygen === void 0 ? void 0 : Oxygen.env) === null || _a === void 0 ? void 0 : _a.SHOPIFY_STOREFRONT_API_SECRET_TOKEN
102
103
  : null;
103
104
  const buyerIp = request.getBuyerIp();
104
105
  const extraHeaders = {};
106
+ /**
107
+ * Only pass one type of storefront token at a time.
108
+ */
109
+ if (secretToken) {
110
+ extraHeaders['Shopify-Storefront-Private-Token'] = secretToken;
111
+ }
112
+ else {
113
+ extraHeaders['X-Shopify-Storefront-Access-Token'] = storefrontToken;
114
+ }
105
115
  if (buyerIp) {
106
116
  extraHeaders['Shopify-Storefront-Buyer-IP'] = buyerIp;
107
117
  }
108
118
  return {
109
- key: [storeDomain, storefrontApiVersion, body, locale],
119
+ key: [storeDomain, storefrontApiVersion, body],
110
120
  url: `https://${storeDomain}/api/${storefrontApiVersion}/graphql.json`,
111
121
  requestInit: {
112
122
  body,
113
123
  method: 'POST',
114
124
  headers: {
115
- 'X-Shopify-Storefront-Access-Token': secretToken !== null && secretToken !== void 0 ? secretToken : storefrontToken,
116
125
  'X-SDK-Variant': 'hydrogen',
117
126
  'X-SDK-Version': storefrontApiVersion,
118
127
  'content-type': 'application/json',
119
- 'Accept-Language': (_b = locale) !== null && _b !== void 0 ? _b : defaultLocale,
120
128
  ...extraHeaders,
121
129
  },
122
130
  },