@shopify/hydrogen 0.6.4 → 0.7.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 (73) hide show
  1. package/dist/esnext/components/AddToCartButton/AddToCartButton.client.d.ts +1 -1
  2. package/dist/esnext/components/AddToCartButton/AddToCartButton.client.js +3 -3
  3. package/dist/esnext/components/Metafield/Metafield.client.js +8 -0
  4. package/dist/esnext/components/Metafield/MetafieldFragment.d.ts +16 -1
  5. package/dist/esnext/components/Model3D/Model3D.client.d.ts +2 -0
  6. package/dist/esnext/components/Model3D/Model3D.client.js +1 -1
  7. package/dist/esnext/components/ProductProvider/context.d.ts +2 -2
  8. package/dist/esnext/components/ProductProvider/types.d.ts +2 -2
  9. package/dist/esnext/components/SelectedVariantAddToCartButton/SelectedVariantAddToCartButton.client.d.ts +1 -1
  10. package/dist/esnext/components/SelectedVariantAddToCartButton/SelectedVariantAddToCartButton.client.js +1 -1
  11. package/dist/esnext/entry-server.js +43 -19
  12. package/dist/esnext/foundation/ServerStateProvider/ServerStateProvider.client.d.ts +19 -11
  13. package/dist/esnext/foundation/ServerStateProvider/ServerStateProvider.client.js +21 -20
  14. package/dist/esnext/foundation/ServerStateProvider/index.d.ts +1 -1
  15. package/dist/esnext/foundation/ShopifyProvider/ShopifyServerProvider.server.js +1 -1
  16. package/dist/esnext/{hooks → foundation}/useQuery/QueryProvider.d.ts +0 -0
  17. package/dist/esnext/{hooks → foundation}/useQuery/QueryProvider.js +0 -0
  18. package/dist/esnext/{hooks → foundation}/useQuery/hooks.d.ts +0 -0
  19. package/dist/esnext/{hooks → foundation}/useQuery/hooks.js +13 -3
  20. package/dist/esnext/{hooks → foundation}/useQuery/index.d.ts +0 -0
  21. package/dist/esnext/{hooks → foundation}/useQuery/index.js +0 -0
  22. package/dist/esnext/foundation/useServerState/use-server-state.d.ts +1 -1
  23. package/dist/esnext/framework/Hydration/ServerComponentResponse.server.d.ts +10 -0
  24. package/dist/esnext/framework/Hydration/ServerComponentResponse.server.js +13 -0
  25. package/dist/esnext/framework/middleware.d.ts +5 -1
  26. package/dist/esnext/framework/middleware.js +10 -5
  27. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-config.js +4 -1
  28. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-middleware.js +7 -1
  29. package/dist/esnext/framework/plugins/vite-plugin-react-server-components-shim.js +11 -1
  30. package/dist/esnext/graphql/graphql-constants.d.ts +51 -14
  31. package/dist/esnext/graphql/graphql-constants.js +89 -15
  32. package/dist/esnext/handle-event.js +8 -4
  33. package/dist/esnext/hooks/index.d.ts +1 -1
  34. package/dist/esnext/hooks/index.js +1 -1
  35. package/dist/esnext/hooks/useMoney/hooks.js +2 -16
  36. package/dist/esnext/hooks/useParsedMetafields/useParsedMetafields.d.ts +2 -3
  37. package/dist/esnext/hooks/useProductOptions/types.d.ts +2 -3
  38. package/dist/esnext/hooks/useShopQuery/hooks.js +1 -1
  39. package/dist/esnext/types.d.ts +19 -3
  40. package/dist/esnext/utilities/flattenConnection/flattenConnection.d.ts +0 -7
  41. package/dist/esnext/utilities/flattenConnection/flattenConnection.js +0 -7
  42. package/dist/esnext/utilities/isClient/isClient.d.ts +0 -4
  43. package/dist/esnext/utilities/isClient/isClient.js +0 -4
  44. package/dist/esnext/utilities/isServer/isServer.d.ts +0 -4
  45. package/dist/esnext/utilities/isServer/isServer.js +0 -4
  46. package/dist/esnext/utilities/parseMetafieldValue/parseMetafieldValue.d.ts +2 -33
  47. package/dist/esnext/utilities/parseMetafieldValue/parseMetafieldValue.js +0 -31
  48. package/dist/esnext/version.d.ts +1 -1
  49. package/dist/esnext/version.js +1 -1
  50. package/dist/node/framework/Hydration/ServerComponentResponse.server.d.ts +10 -0
  51. package/dist/node/framework/Hydration/ServerComponentResponse.server.js +13 -0
  52. package/dist/node/framework/middleware.d.ts +5 -1
  53. package/dist/node/framework/middleware.js +13 -6
  54. package/dist/node/framework/plugins/vite-plugin-hydrogen-config.js +4 -1
  55. package/dist/node/framework/plugins/vite-plugin-hydrogen-middleware.js +8 -2
  56. package/dist/node/framework/plugins/vite-plugin-react-server-components-shim.js +11 -1
  57. package/dist/node/handle-event.js +8 -4
  58. package/dist/node/types.d.ts +19 -3
  59. package/dist/node/utilities/flattenConnection/flattenConnection.d.ts +0 -7
  60. package/dist/node/utilities/flattenConnection/flattenConnection.js +0 -7
  61. package/dist/node/utilities/isClient/isClient.d.ts +0 -4
  62. package/dist/node/utilities/isClient/isClient.js +0 -4
  63. package/dist/node/utilities/isServer/isServer.d.ts +0 -4
  64. package/dist/node/utilities/isServer/isServer.js +0 -4
  65. package/dist/node/utilities/parseMetafieldValue/parseMetafieldValue.d.ts +2 -33
  66. package/dist/node/utilities/parseMetafieldValue/parseMetafieldValue.js +0 -31
  67. package/dist/node/version.d.ts +1 -1
  68. package/dist/node/version.js +1 -1
  69. package/dist/worker/framework/Hydration/ServerComponentResponse.server.d.ts +10 -0
  70. package/dist/worker/framework/Hydration/ServerComponentResponse.server.js +13 -0
  71. package/dist/worker/handle-event.js +8 -4
  72. package/dist/worker/types.d.ts +19 -3
  73. package/package.json +7 -5
@@ -7,7 +7,7 @@ export interface AddToCartButtonProps {
7
7
  value: string;
8
8
  }[];
9
9
  /** The ID of the variant. */
10
- variantID: string;
10
+ variantId: string;
11
11
  /** The item quantity. */
12
12
  quantity?: number;
13
13
  /** Any ReactNode elements. */
@@ -6,7 +6,7 @@ import { useCartLinesAddCallback, useCart, useCartCreateCallback, } from '../Car
6
6
  */
7
7
  export function AddToCartButton(props) {
8
8
  const [addingItem, setAddingItem] = useState(false);
9
- const { variantID, quantity = 1, attributes, children, onAdd, accessibleAddingToCartLabel, ...passthroughProps } = props;
9
+ const { variantId, quantity = 1, attributes, children, onAdd, accessibleAddingToCartLabel, ...passthroughProps } = props;
10
10
  const { status, id } = useCart();
11
11
  const createCart = useCartCreateCallback();
12
12
  const addLines = useCartLinesAddCallback();
@@ -23,7 +23,7 @@ export function AddToCartButton(props) {
23
23
  lines: [
24
24
  {
25
25
  quantity: quantity,
26
- merchandiseId: variantID,
26
+ merchandiseId: variantId,
27
27
  attributes: attributes,
28
28
  },
29
29
  ],
@@ -33,7 +33,7 @@ export function AddToCartButton(props) {
33
33
  addLines([
34
34
  {
35
35
  quantity: quantity,
36
- merchandiseId: variantID,
36
+ merchandiseId: variantId,
37
37
  attributes: attributes,
38
38
  },
39
39
  ]);
@@ -4,6 +4,7 @@ import { getMeasurementAsString } from '../../utilities';
4
4
  import { StarRating } from './components/StarRating';
5
5
  import { RawHtml } from '../RawHtml';
6
6
  import { MetafieldFragment as Fragment } from '../../graphql/graphql-constants';
7
+ import { Image } from '../Image';
7
8
  /**
8
9
  * The `Metafield` component renders the value of a Storefront
9
10
  * API's [Metafield object](/api/storefront/reference/common-objects/metafield).
@@ -16,6 +17,7 @@ import { MetafieldFragment as Fragment } from '../../graphql/graphql-constants';
16
17
  * Metafield's `value`. For more information, refer to the [Default Output](#default-output) section.
17
18
  */
18
19
  export function Metafield(props) {
20
+ var _a;
19
21
  const { metafield, children, as, ...passthroughProps } = props;
20
22
  const { locale } = useShop();
21
23
  if (metafield.value == null) {
@@ -54,6 +56,12 @@ export function Metafield(props) {
54
56
  case 'json':
55
57
  const Wrapper = as !== null && as !== void 0 ? as : 'span';
56
58
  return (React.createElement(Wrapper, { ...passthroughProps }, JSON.stringify(metafield.value)));
59
+ case 'file_reference': {
60
+ if (((_a = metafield.reference) === null || _a === void 0 ? void 0 : _a.__typename) === 'MediaImage') {
61
+ const ref = metafield.reference;
62
+ return ref.image ? (React.createElement(Image, { image: ref.image, ...passthroughProps })) : null;
63
+ }
64
+ }
57
65
  default: {
58
66
  const Wrapper = as !== null && as !== void 0 ? as : 'span';
59
67
  return (React.createElement(Wrapper, { ...passthroughProps }, metafield.value.toString()));
@@ -1,4 +1,19 @@
1
1
  import * as Types from '../../graphql/types/types';
2
+ import { ImageFragmentFragment } from '../Image/ImageFragment';
2
3
  export declare type MetafieldFragmentFragment = {
3
4
  __typename?: 'Metafield';
4
- } & Pick<Types.Metafield, 'id' | 'type' | 'namespace' | 'key' | 'value' | 'createdAt' | 'updatedAt' | 'description'>;
5
+ } & Pick<Types.Metafield, 'id' | 'type' | 'namespace' | 'key' | 'value' | 'createdAt' | 'updatedAt' | 'description'> & {
6
+ reference?: Types.Maybe<({
7
+ __typename: 'MediaImage';
8
+ } & Pick<Types.MediaImage, 'id' | 'mediaContentType'> & {
9
+ image?: Types.Maybe<{
10
+ __typename?: 'Image';
11
+ } & ImageFragmentFragment>;
12
+ }) | {
13
+ __typename: 'Page';
14
+ } | {
15
+ __typename: 'Product';
16
+ } | {
17
+ __typename: 'ProductVariant';
18
+ }>;
19
+ };
@@ -15,6 +15,8 @@ export interface Model3DProps {
15
15
  };
16
16
  /** A string of either `auto`, `lazy`, or `eager` to indicate the conditions for preloading. Refer to [`loading` in the <model-viewer> documentation](https://modelviewer.dev/docs/index.html#entrydocs-loading-attributes-loading). */
17
17
  loading?: 'auto' | 'lazy' | 'eager';
18
+ /** A url to display an image instead of the model, useful for showing the user something before a model is loaded and ready to render. If none is provided, [Model3d.previewImage](https://shopify.dev/api/storefront/reference/products/model3d#previewimage-2021-10) is used. Refer to [`poster` in the <model-viewer> documentation](https://modelviewer.dev/docs/index.html#entrydocs-loading-attributes-poster). */
19
+ poster?: string;
18
20
  /** A string of either `auto`, `interaction`, or `manual` to indicate when the model should be revealed. Refer to [`reveal` in the <model-viewer> documentation](https://modelviewer.dev/docs/index.html#entrydocs-loading-attributes-reveal). */
19
21
  reveal?: 'auto' | 'interaction' | 'manual';
20
22
  /** A boolean to enable an AR experience. Refer to [`ar` in the <model-viewer> documentation](https://modelviewer.dev/docs/index.html#entrydocs-augmentedreality-attributes-ar). */
@@ -109,7 +109,7 @@ export function Model3D(props) {
109
109
  // TODO: What do we want to display while the model-viewer library loads?
110
110
  return null;
111
111
  }
112
- return (React.createElement("model-viewer", { ref: callbackRef, ...passthroughProps, class: className, id: id, src: model.sources[0].url, alt: model.alt, "camera-controls": (_a = passthroughProps.cameraControls) !== null && _a !== void 0 ? _a : true, poster: (_b = model.previewImage) === null || _b === void 0 ? void 0 : _b.url, autoplay: (_c = passthroughProps.autoplay) !== null && _c !== void 0 ? _c : true, loading: passthroughProps.loading, reveal: passthroughProps.reveal, ar: passthroughProps.ar, "ar-modes": passthroughProps.arModes, "ar-scale": passthroughProps.arScale, "ar-placement": passthroughProps.arPlacement, "ios-src": passthroughProps.iosSrc, "touch-action": passthroughProps.touchAction, "disable-zoom": passthroughProps.disableZoom, "orbit-sensitivity": passthroughProps.orbitSensitivity, "auto-rotate": passthroughProps.autoRotate, "auto-rotate-delay": passthroughProps.autoRotateDelay, "rotation-per-second": passthroughProps.rotationPerSecond, "interaction-policy": passthroughProps.interactionPolicy, "interaction-prompt": passthroughProps.interactionPrompt, "interaction-prompt-style": passthroughProps.interactionPromptStyle, "interaction-prompt-threshold": passthroughProps.interactionPromptThreshold, "camera-orbit": passthroughProps.cameraOrbit, "camera-target": passthroughProps.cameraTarget, "field-of-view": passthroughProps.fieldOfView, "max-camera-orbit": passthroughProps.maxCameraOrbit, "min-camera-orbit": passthroughProps.minCameraOrbit, "max-field-of-view": passthroughProps.maxFieldOfView, "min-field-of-view": passthroughProps.minFieldOfView, bounds: passthroughProps.bounds, "interpolation-decay": (_d = passthroughProps.interpolationDecay) !== null && _d !== void 0 ? _d : 100, "skybox-image": passthroughProps.skyboxImage, "environment-image": passthroughProps.environmentImage, exposure: passthroughProps.exposure, "shadow-intensity": (_e = passthroughProps.shadowIntensity) !== null && _e !== void 0 ? _e : 0, "shadow-softness": (_f = passthroughProps.shadowSoftness) !== null && _f !== void 0 ? _f : 0, "animation-name": passthroughProps.animationName, "animation-crossfade-duration": passthroughProps.animationCrossfadeDuration, "variant-name": passthroughProps.variantName, orientation: passthroughProps.orientation, scale: passthroughProps.scale }, children));
112
+ return (React.createElement("model-viewer", { ref: callbackRef, ...passthroughProps, class: className, id: id, src: model.sources[0].url, alt: model.alt, "camera-controls": (_a = passthroughProps.cameraControls) !== null && _a !== void 0 ? _a : true, poster: passthroughProps.poster || ((_b = model.previewImage) === null || _b === void 0 ? void 0 : _b.url), autoplay: (_c = passthroughProps.autoplay) !== null && _c !== void 0 ? _c : true, loading: passthroughProps.loading, reveal: passthroughProps.reveal, ar: passthroughProps.ar, "ar-modes": passthroughProps.arModes, "ar-scale": passthroughProps.arScale, "ar-placement": passthroughProps.arPlacement, "ios-src": passthroughProps.iosSrc, "touch-action": passthroughProps.touchAction, "disable-zoom": passthroughProps.disableZoom, "orbit-sensitivity": passthroughProps.orbitSensitivity, "auto-rotate": passthroughProps.autoRotate, "auto-rotate-delay": passthroughProps.autoRotateDelay, "rotation-per-second": passthroughProps.rotationPerSecond, "interaction-policy": passthroughProps.interactionPolicy, "interaction-prompt": passthroughProps.interactionPrompt, "interaction-prompt-style": passthroughProps.interactionPromptStyle, "interaction-prompt-threshold": passthroughProps.interactionPromptThreshold, "camera-orbit": passthroughProps.cameraOrbit, "camera-target": passthroughProps.cameraTarget, "field-of-view": passthroughProps.fieldOfView, "max-camera-orbit": passthroughProps.maxCameraOrbit, "min-camera-orbit": passthroughProps.minCameraOrbit, "max-field-of-view": passthroughProps.maxFieldOfView, "min-field-of-view": passthroughProps.minFieldOfView, bounds: passthroughProps.bounds, "interpolation-decay": (_d = passthroughProps.interpolationDecay) !== null && _d !== void 0 ? _d : 100, "skybox-image": passthroughProps.skyboxImage, "environment-image": passthroughProps.environmentImage, exposure: passthroughProps.exposure, "shadow-intensity": (_e = passthroughProps.shadowIntensity) !== null && _e !== void 0 ? _e : 0, "shadow-softness": (_f = passthroughProps.shadowSoftness) !== null && _f !== void 0 ? _f : 0, "animation-name": passthroughProps.animationName, "animation-crossfade-duration": passthroughProps.animationCrossfadeDuration, "variant-name": passthroughProps.variantName, orientation: passthroughProps.orientation, scale: passthroughProps.scale }, children));
113
113
  }
114
114
  Model3D.Fragment = Fragment;
115
115
  export const Model3DFragment = Fragment;
@@ -1,5 +1,5 @@
1
1
  import { ProductOptionsHookValue } from '../../hooks';
2
- import { GraphQLConnection, ParsedMetafield } from '../../types';
2
+ import { GraphQLConnection, ParsedMetafield, RawMetafield } from '../../types';
3
3
  import { ProductProviderFragmentFragment } from './ProductProviderFragment';
4
4
  import { Product } from './types';
5
5
  import { Collection, Image } from '../../graphql/types/types';
@@ -8,7 +8,7 @@ export declare type ProductContextType = Omit<Product, 'media' | 'metafields' |
8
8
  media?: ProductProviderFragmentFragment['media']['edges'][0]['node'][];
9
9
  mediaConnection?: ProductProviderFragmentFragment['media'];
10
10
  metafields?: ParsedMetafield[];
11
- metafieldsConnection?: ProductProviderFragmentFragment['metafields'];
11
+ metafieldsConnection?: GraphQLConnection<RawMetafield>;
12
12
  images?: Partial<Image>[];
13
13
  imagesConnection?: GraphQLConnection<Partial<Image>>;
14
14
  collections?: Partial<Collection>[];
@@ -1,5 +1,5 @@
1
1
  import { SellingPlanGroup, Variant } from '../../hooks/useProductOptions';
2
- import { GraphQLConnection } from '../../types';
2
+ import { GraphQLConnection, RawMetafield } from '../../types';
3
3
  import { ProductProviderFragmentFragment } from './ProductProviderFragment';
4
4
  import { ImageFragmentFragment } from '../Image/ImageFragment';
5
5
  import { Collection } from '../../graphql/types/types';
@@ -9,7 +9,7 @@ export interface Product {
9
9
  handle?: ProductProviderFragmentFragment['descriptionHtml'];
10
10
  id?: ProductProviderFragmentFragment['id'];
11
11
  media?: ProductProviderFragmentFragment['media'];
12
- metafields?: ProductProviderFragmentFragment['metafields'];
12
+ metafields?: GraphQLConnection<RawMetafield>;
13
13
  priceRange?: Partial<ProductProviderFragmentFragment['priceRange']>;
14
14
  title?: ProductProviderFragmentFragment['title'];
15
15
  variants?: GraphQLConnection<Variant>;
@@ -7,5 +7,5 @@ declare type PropsWeControl = 'disabled';
7
7
  * selected variant. Clicking this button automatically adds the selected variant to the cart.
8
8
  * It must be a descendent of a `ProductProvider` and `CartProvider` component.
9
9
  */
10
- export declare function SelectedVariantAddToCartButton<TTag extends React.ElementType = 'button'>(props: Props<TTag, PropsWeControl> & Omit<AddToCartButtonProps, 'variantID'>): JSX.Element;
10
+ export declare function SelectedVariantAddToCartButton<TTag extends React.ElementType = 'button'>(props: Props<TTag, PropsWeControl> & Omit<AddToCartButtonProps, 'variantId'>): JSX.Element;
11
11
  export {};
@@ -13,5 +13,5 @@ export function SelectedVariantAddToCartButton(props) {
13
13
  throw new Error('Expected a Product context, but none was found');
14
14
  }
15
15
  const { children, quantity, attributes, ...passthroughProps } = props;
16
- return (React.createElement(CartButton, { ...passthroughProps, variantID: (_b = (_a = product.selectedVariant) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : '', quantity: quantity !== null && quantity !== void 0 ? quantity : 1, disabled: !product.selectedVariant || passthroughProps.disabled, attributes: attributes }, children));
16
+ return (React.createElement(CartButton, { ...passthroughProps, variantId: (_b = (_a = product.selectedVariant) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : '', quantity: quantity !== null && quantity !== void 0 ? quantity : 1, disabled: !product.selectedVariant || passthroughProps.disabled, attributes: attributes }, children));
17
17
  }
@@ -84,26 +84,23 @@ const renderHydrogen = (App, hook) => {
84
84
  * additional checks downstream?
85
85
  */
86
86
  response.setHeader(getCacheControlHeader({ dev }), componentResponse.cacheControlHeader);
87
+ writeHeadToServerResponse(response, componentResponse, didError);
88
+ if (isRedirect(response)) {
89
+ // Return redirects early without further rendering/streaming
90
+ return response.end();
91
+ }
87
92
  if (!componentResponse.canStream())
88
93
  return;
89
- response.statusCode = didError ? 500 : 200;
90
- response.setHeader('Content-type', 'text/html');
91
- response.write('<!DOCTYPE html>');
92
- startWriting();
93
- if (dev && didError) {
94
- // This error was delayed until the headers were properly sent.
95
- response.write(getErrorMarkup(didError));
96
- }
94
+ startWritingHtmlToServerResponse(response, startWriting, dev ? didError : undefined);
97
95
  },
98
96
  onCompleteAll() {
99
- var _a;
100
- if (componentResponse.canStream())
97
+ if (componentResponse.canStream() || response.writableEnded)
101
98
  return;
102
- response.statusCode =
103
- (_a = componentResponse.status) !== null && _a !== void 0 ? _a : (didError ? 500 : 200);
104
- componentResponse.headers.forEach((value, header) => {
105
- response.setHeader(header, value);
106
- });
99
+ writeHeadToServerResponse(response, componentResponse, didError);
100
+ if (isRedirect(response)) {
101
+ // Redirects found after any async code
102
+ return response.end();
103
+ }
107
104
  if (componentResponse.customBody) {
108
105
  if (componentResponse.customBody instanceof Promise) {
109
106
  componentResponse.customBody.then((body) => response.end(body));
@@ -113,9 +110,7 @@ const renderHydrogen = (App, hook) => {
113
110
  }
114
111
  }
115
112
  else {
116
- response.setHeader('Content-type', 'text/html');
117
- response.write('<!DOCTYPE html>');
118
- startWriting();
113
+ startWritingHtmlToServerResponse(response, startWriting, dev ? didError : undefined);
119
114
  }
120
115
  },
121
116
  onError(error) {
@@ -180,7 +175,7 @@ function buildReactApp({ App, state, context, request, dev, }) {
180
175
  const componentResponse = new ServerComponentResponse();
181
176
  const ReactApp = (props) => (React.createElement(StaticRouter, { location: { pathname: state.pathname, search: state.search }, context: context },
182
177
  React.createElement(HelmetProvider, { context: helmetContext },
183
- React.createElement(App, { request: request, response: componentResponse, ...props }))));
178
+ React.createElement(App, { ...props, request: request, response: componentResponse }))));
184
179
  return { helmetContext, ReactApp, componentResponse };
185
180
  }
186
181
  function extractHeadElements(helmetContext) {
@@ -310,3 +305,32 @@ async function renderAppFromStringWithPrepass(ReactApp, state, isReactHydrationR
310
305
  : body;
311
306
  }
312
307
  export default renderHydrogen;
308
+ function startWritingHtmlToServerResponse(response, startWriting, error) {
309
+ if (!response.headersSent) {
310
+ response.setHeader('Content-type', 'text/html');
311
+ response.write('<!DOCTYPE html>');
312
+ }
313
+ startWriting();
314
+ if (error) {
315
+ // This error was delayed until the headers were properly sent.
316
+ response.write(getErrorMarkup(error));
317
+ }
318
+ }
319
+ function writeHeadToServerResponse(response, { headers, status, customStatus }, error) {
320
+ var _a, _b;
321
+ if (response.headersSent)
322
+ return;
323
+ headers.forEach((value, key) => response.setHeader(key, value));
324
+ if (error) {
325
+ response.statusCode = 500;
326
+ }
327
+ else {
328
+ response.statusCode = (_b = (_a = customStatus === null || customStatus === void 0 ? void 0 : customStatus.code) !== null && _a !== void 0 ? _a : status) !== null && _b !== void 0 ? _b : 200;
329
+ if (customStatus === null || customStatus === void 0 ? void 0 : customStatus.text) {
330
+ response.statusMessage = customStatus.text;
331
+ }
332
+ }
333
+ }
334
+ function isRedirect(response) {
335
+ return response.statusCode >= 300 && response.statusCode < 400;
336
+ }
@@ -1,17 +1,25 @@
1
1
  import React, { ReactNode } from 'react';
2
+ declare global {
3
+ var __DEV__: boolean;
4
+ }
5
+ export interface ServerState {
6
+ pathname: string;
7
+ search: string;
8
+ [key: string]: any;
9
+ }
10
+ export interface ServerStateSetter {
11
+ (input: ((prev: ServerState) => Partial<ServerState>) | Partial<ServerState> | string, propValue?: any): void;
12
+ }
2
13
  export interface ServerStateContextValue {
3
- serverState: Record<string, any>;
4
- setServerState(input: (() => void) | Record<string, any> | string, value?: string | Record<string, any>): void;
5
- pending: any;
14
+ pending: boolean;
15
+ serverState: ServerState;
16
+ setServerState: ServerStateSetter;
6
17
  }
7
- export declare const ServerStateContext: React.Context<any>;
8
- interface Props {
9
- serverState: Record<string, any>;
10
- setServerState: React.Dispatch<React.SetStateAction<{
11
- pathname: string;
12
- search: string;
13
- }>>;
18
+ export declare const ServerStateContext: React.Context<ServerStateContextValue>;
19
+ interface ServerStateProviderProps {
20
+ serverState: ServerState;
21
+ setServerState: React.Dispatch<React.SetStateAction<ServerState>>;
14
22
  children: ReactNode;
15
23
  }
16
- export declare function ServerStateProvider({ serverState, setServerState, children, }: Props): JSX.Element;
24
+ export declare function ServerStateProvider({ serverState, setServerState, children, }: ServerStateProviderProps): JSX.Element;
17
25
  export {};
@@ -4,7 +4,7 @@ useTransition, } from 'react';
4
4
  export const ServerStateContext = createContext(null);
5
5
  export function ServerStateProvider({ serverState, setServerState, children, }) {
6
6
  const [pending, startTransition] = useTransition();
7
- const setServerStateCallback = useCallback((input, value) => {
7
+ const setServerStateCallback = useCallback((input, propValue) => {
8
8
  /**
9
9
  * By wrapping this state change in a transition, React renders the new state
10
10
  * concurrently in a new "tree" instead of Suspending and showing the (blank)
@@ -13,26 +13,27 @@ export function ServerStateProvider({ serverState, setServerState, children, })
13
13
  * the `pending` flag also provided by the hook to display in the UI.
14
14
  */
15
15
  startTransition(() => {
16
- // Support callback-style setState
17
- if (typeof input === 'function') {
18
- // @ts-ignore
19
- return setServerState(input);
20
- }
21
- // Support a simple object, and spread it into the existing object.
22
- if (typeof input === 'object') {
23
- return setServerState((prev) => ({
16
+ return setServerState((prev) => {
17
+ let newValue;
18
+ if (typeof input === 'function') {
19
+ newValue = input(prev);
20
+ }
21
+ else if (typeof input === 'string') {
22
+ newValue = { [input]: propValue };
23
+ }
24
+ else {
25
+ newValue = input;
26
+ }
27
+ if (__DEV__) {
28
+ if ('request' in newValue || 'response' in newValue) {
29
+ console.warn(`Custom "request" and "response" properties in server state are ignored. Use a different name.`);
30
+ }
31
+ }
32
+ return {
24
33
  ...prev,
25
- // @ts-ignore
26
- ...input,
27
- }));
28
- }
29
- // Support a key, value as well.
30
- if (typeof input === 'string') {
31
- return setServerState((prev) => ({
32
- ...prev,
33
- [input]: value,
34
- }));
35
- }
34
+ ...newValue,
35
+ };
36
+ });
36
37
  });
37
38
  }, [setServerState, startTransition]);
38
39
  const value = useMemo(() => ({
@@ -1 +1 @@
1
- export { ServerStateProvider, ServerStateContext, ServerStateContextValue, } from './ServerStateProvider.client';
1
+ export { ServerStateProvider, ServerStateContext, ServerStateContextValue, ServerState, } from './ServerStateProvider.client';
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { ShopifyProvider } from './ShopifyProvider';
3
- import { QueryProvider } from '../../hooks/useQuery';
3
+ import { QueryProvider } from '../../foundation/useQuery';
4
4
  export function ShopifyServerProvider({ children, shopifyConfig, hydrationContext, }) {
5
5
  return (React.createElement(ShopifyProvider, { shopifyConfig: shopifyConfig },
6
6
  React.createElement(QueryProvider, { hydrationContext: hydrationContext }, children)));
@@ -11,6 +11,16 @@ key,
11
11
  queryFn,
12
12
  /** Options including `cache` to manage the cache behavior of the sub-request. */
13
13
  queryOptions) {
14
+ const resolvedQueryOptions = {
15
+ /**
16
+ * Prevent react-query from from retrying request failures. This sometimes bites developers
17
+ * because they will get back a 200 GraphQL response with errors, but not properly check
18
+ * for errors. This leads to a failed `queryFn` and react-query keeps running it, leading
19
+ * to a much slower response time and a poor developer experience.
20
+ */
21
+ retry: false,
22
+ ...(queryOptions !== null && queryOptions !== void 0 ? queryOptions : {}),
23
+ };
14
24
  /**
15
25
  * Attempt to read the query from cache. If it doesn't exist or if it's stale, regenerate it.
16
26
  */
@@ -35,7 +45,7 @@ queryOptions) {
35
45
  await setItemInCache(lockKey, true);
36
46
  try {
37
47
  const output = await generateNewOutput();
38
- await setItemInCache(key, output, queryOptions === null || queryOptions === void 0 ? void 0 : queryOptions.cache);
48
+ await setItemInCache(key, output, resolvedQueryOptions === null || resolvedQueryOptions === void 0 ? void 0 : resolvedQueryOptions.cache);
39
49
  }
40
50
  catch (e) {
41
51
  console.error(`Error generating async response: ${e.message}`);
@@ -51,8 +61,8 @@ queryOptions) {
51
61
  /**
52
62
  * Important: Do this async
53
63
  */
54
- runDelayedFunction(async () => await setItemInCache(key, newOutput, queryOptions === null || queryOptions === void 0 ? void 0 : queryOptions.cache));
64
+ runDelayedFunction(async () => await setItemInCache(key, newOutput, resolvedQueryOptions === null || resolvedQueryOptions === void 0 ? void 0 : resolvedQueryOptions.cache));
55
65
  return newOutput;
56
66
  }
57
- return useReactQuery(key, cachedQueryFn, queryOptions);
67
+ return useReactQuery(key, cachedQueryFn, resolvedQueryOptions);
58
68
  }
@@ -12,4 +12,4 @@
12
12
  * | `pending` | Whether a [transition is pending](https://github.com/reactwg/react-18/discussions/41). |
13
13
  *
14
14
  */
15
- export declare function useServerState(): any;
15
+ export declare function useServerState(): import("../ServerStateProvider").ServerStateContextValue;
@@ -2,6 +2,10 @@ import { CacheOptions } from '../../types';
2
2
  export declare class ServerComponentResponse extends Response {
3
3
  private wait;
4
4
  private cacheOptions?;
5
+ customStatus?: {
6
+ code?: number;
7
+ text?: string;
8
+ };
5
9
  /**
6
10
  * Allow custom body to be a string or a Promise.
7
11
  */
@@ -14,6 +18,12 @@ export declare class ServerComponentResponse extends Response {
14
18
  canStream(): boolean;
15
19
  cache(options: CacheOptions): void;
16
20
  get cacheControlHeader(): string;
21
+ writeHead({ status, statusText, headers, }?: {
22
+ status?: number;
23
+ statusText?: string;
24
+ headers?: Record<string, any>;
25
+ }): void;
26
+ redirect(location: string, status?: number): void;
17
27
  /**
18
28
  * Send the response from a Server Component. Renders React components to string,
19
29
  * and returns `null` to make React happy.
@@ -30,6 +30,19 @@ export class ServerComponentResponse extends Response {
30
30
  };
31
31
  return generateCacheControlHeader(options);
32
32
  }
33
+ writeHead({ status, statusText, headers, } = {}) {
34
+ if (status || statusText) {
35
+ this.customStatus = { code: status, text: statusText };
36
+ }
37
+ if (headers) {
38
+ for (const [key, value] of Object.entries(headers)) {
39
+ this.headers.set(key, value);
40
+ }
41
+ }
42
+ }
43
+ redirect(location, status = 307) {
44
+ this.writeHead({ status, headers: { location } });
45
+ }
33
46
  /**
34
47
  * Send the response from a Server Component. Renders React components to string,
35
48
  * and returns `null` to make React happy.
@@ -10,9 +10,13 @@ declare type HydrogenMiddlewareArgs = {
10
10
  devServer?: ViteDevServer;
11
11
  cache?: Cache;
12
12
  };
13
+ export declare function graphiqlMiddleware({ shopifyConfig, dev, }: {
14
+ shopifyConfig: ShopifyConfig;
15
+ dev: boolean;
16
+ }): (request: IncomingMessage, response: http.ServerResponse, next: NextFunction) => Promise<void>;
13
17
  /**
14
18
  * Provides middleware to Node.js Express-like servers. Used by the Hydrogen
15
19
  * Vite dev server plugin as well as production Node.js implementation.
16
20
  */
17
- export default function hydrogenMiddleware({ dev, shopifyConfig, cache, indexTemplate, getServerEntrypoint, devServer, }: HydrogenMiddlewareArgs): (request: IncomingMessage, response: http.ServerResponse, next: NextFunction) => Promise<void>;
21
+ export declare function hydrogenMiddleware({ dev, cache, indexTemplate, getServerEntrypoint, devServer, }: HydrogenMiddlewareArgs): (request: IncomingMessage, response: http.ServerResponse, next: NextFunction) => Promise<void>;
18
22
  export {};
@@ -1,15 +1,20 @@
1
1
  import { graphiqlHtml } from './graphiql';
2
2
  import handleEvent from '../handle-event';
3
- /**
4
- * Provides middleware to Node.js Express-like servers. Used by the Hydrogen
5
- * Vite dev server plugin as well as production Node.js implementation.
6
- */
7
- export default function hydrogenMiddleware({ dev, shopifyConfig, cache, indexTemplate, getServerEntrypoint, devServer, }) {
3
+ export function graphiqlMiddleware({ shopifyConfig, dev, }) {
8
4
  return async function (request, response, next) {
9
5
  const graphiqlRequest = dev && isGraphiqlRequest(request);
10
6
  if (graphiqlRequest) {
11
7
  return respondWithGraphiql(response, shopifyConfig);
12
8
  }
9
+ next();
10
+ };
11
+ }
12
+ /**
13
+ * Provides middleware to Node.js Express-like servers. Used by the Hydrogen
14
+ * Vite dev server plugin as well as production Node.js implementation.
15
+ */
16
+ export function hydrogenMiddleware({ dev, cache, indexTemplate, getServerEntrypoint, devServer, }) {
17
+ return async function (request, response, next) {
13
18
  const url = new URL('http://' + request.headers.host + request.originalUrl);
14
19
  const isReactHydrationRequest = url.pathname === '/react';
15
20
  /**
@@ -1,7 +1,7 @@
1
1
  export default () => {
2
2
  return {
3
3
  name: 'vite-plugin-hydrogen-config',
4
- config: () => ({
4
+ config: (_, env) => ({
5
5
  resolve: {
6
6
  alias: {
7
7
  /**
@@ -52,6 +52,9 @@ export default () => {
52
52
  'react-router-dom',
53
53
  ],
54
54
  },
55
+ define: {
56
+ __DEV__: env.mode !== 'production',
57
+ },
55
58
  }),
56
59
  };
57
60
  };
@@ -1,6 +1,6 @@
1
1
  import path from 'path';
2
2
  import { promises as fs } from 'fs';
3
- import hydrogenMiddleware from '../middleware';
3
+ import { hydrogenMiddleware, graphiqlMiddleware } from '../middleware';
4
4
  import { InMemoryCache } from '../cache/in-memory';
5
5
  export default (shopifyConfig, pluginOptions) => {
6
6
  return {
@@ -17,6 +17,12 @@ export default (shopifyConfig, pluginOptions) => {
17
17
  const indexHtml = await fs.readFile(resolve('index.html'), 'utf-8');
18
18
  return await server.transformIndexHtml(url, indexHtml);
19
19
  }
20
+ // The default vite middleware rewrites the URL `/graphqil` to `/index.html`
21
+ // By running this middleware first, we avoid that.
22
+ server.middlewares.use(graphiqlMiddleware({
23
+ shopifyConfig,
24
+ dev: true,
25
+ }));
20
26
  return () => server.middlewares.use(hydrogenMiddleware({
21
27
  dev: true,
22
28
  shopifyConfig,
@@ -2,6 +2,7 @@ import { normalizePath } from 'vite';
2
2
  import path from 'path';
3
3
  import { proxyClientComponent } from '../server-components';
4
4
  import { resolve } from './resolver';
5
+ import { promises as fs } from 'fs';
5
6
  export default () => {
6
7
  let config;
7
8
  return {
@@ -34,13 +35,22 @@ export default () => {
34
35
  'When using Hydrogen components within Client Components, use the `@shopify/hydrogen/client` entrypoint instead.');
35
36
  }
36
37
  },
37
- load(id, options) {
38
+ async load(id, options) {
38
39
  if (!isSSR(options))
39
40
  return null;
40
41
  // Wrapped components won't match this becase they end in ?no-proxy
41
42
  if (/\.client\.[jt]sx?$/.test(id)) {
42
43
  return proxyClientComponent({ id });
43
44
  }
45
+ // Temporary fix for sourcemap warnings in client components. This can be fixed in @vitejs/react-plugin.
46
+ // `react-ssr-prepass` sourcemap seems to be broken and crashes in workers/Jest.
47
+ if (id.endsWith('?no-proxy') ||
48
+ id.includes('dist/react-ssr-prepass.es.js')) {
49
+ return {
50
+ code: await fs.readFile(id.split('?')[0], 'utf-8'),
51
+ map: { mappings: '' },
52
+ };
53
+ }
44
54
  return null;
45
55
  },
46
56
  transform(code, id) {