@shopify/hydrogen 1.2.0 → 1.3.2

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 (98) hide show
  1. package/README.md +1 -1
  2. package/dist/esnext/components/CartProvider/CartProvider.client.js +6 -0
  3. package/dist/esnext/components/CartProvider/cart-queries.d.ts +1 -1
  4. package/dist/esnext/components/CartProvider/cart-queries.js +1 -0
  5. package/dist/esnext/components/ExternalVideo/ExternalVideo.js +2 -2
  6. package/dist/esnext/components/Image/Image.d.ts +1 -1
  7. package/dist/esnext/components/Image/Image.js +4 -4
  8. package/dist/esnext/components/Image/index.d.ts +1 -1
  9. package/dist/esnext/components/index.d.ts +1 -1
  10. package/dist/esnext/components/index.js +1 -1
  11. package/dist/esnext/constants.d.ts +1 -0
  12. package/dist/esnext/constants.js +1 -0
  13. package/dist/esnext/entry-client.js +1 -1
  14. package/dist/esnext/entry-server.js +30 -28
  15. package/dist/esnext/experimental.d.ts +1 -0
  16. package/dist/esnext/experimental.js +1 -0
  17. package/dist/esnext/foundation/Cache/cache.js +0 -1
  18. package/dist/esnext/foundation/HydrogenRequest/HydrogenRequest.server.d.ts +1 -0
  19. package/dist/esnext/foundation/HydrogenRequest/HydrogenRequest.server.js +1 -0
  20. package/dist/esnext/foundation/HydrogenResponse/HydrogenResponse.server.d.ts +4 -2
  21. package/dist/esnext/foundation/HydrogenResponse/HydrogenResponse.server.js +17 -16
  22. package/dist/esnext/foundation/ServerPropsProvider/ServerPropsProvider.js +1 -3
  23. package/dist/esnext/foundation/fetchSync/ResponseSync.d.ts +2 -1
  24. package/dist/esnext/foundation/fetchSync/ResponseSync.js +14 -2
  25. package/dist/esnext/foundation/session/session-types.d.ts +2 -0
  26. package/dist/esnext/foundation/session/session.d.ts +3 -0
  27. package/dist/esnext/foundation/session/session.js +16 -0
  28. package/dist/esnext/foundation/useQuery/hooks.d.ts +3 -0
  29. package/dist/esnext/foundation/useQuery/hooks.js +1 -1
  30. package/dist/esnext/foundation/useSession/useSession.d.ts +1 -0
  31. package/dist/esnext/foundation/useSession/useSession.js +13 -0
  32. package/dist/esnext/framework/plugin.js +6 -5
  33. package/dist/esnext/framework/plugins/vite-plugin-assets-version.d.ts +2 -0
  34. package/dist/esnext/framework/plugins/vite-plugin-assets-version.js +12 -0
  35. package/dist/esnext/framework/plugins/vite-plugin-client-imports.js +6 -3
  36. package/dist/esnext/framework/plugins/vite-plugin-css-modules-rsc.js +3 -0
  37. package/dist/esnext/framework/plugins/vite-plugin-css-rsc.js +8 -5
  38. package/dist/esnext/framework/plugins/vite-plugin-hydration-auto-import.js +7 -1
  39. package/dist/{node/framework/plugins/vite-plugin-purge-query-cache.d.ts → esnext/framework/plugins/vite-plugin-hydrogen-client-components-cache.d.ts} +1 -1
  40. package/dist/esnext/framework/plugins/{vite-plugin-hydrogen-client-middleware.js → vite-plugin-hydrogen-client-components-cache.js} +2 -2
  41. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-config.d.ts +2 -1
  42. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-config.js +104 -90
  43. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-middleware.js +6 -6
  44. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-suppress-warnings.js +5 -0
  45. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-virtual-files.js +45 -7
  46. package/dist/esnext/framework/plugins/vite-plugin-platform-entry.js +14 -1
  47. package/dist/esnext/framework/plugins/vite-plugin-ssr-interop.js +5 -1
  48. package/dist/esnext/framework/types.d.ts +1 -0
  49. package/dist/esnext/framework/viteception.js +7 -1
  50. package/dist/esnext/storefront-api-types.d.ts +38 -31
  51. package/dist/esnext/storefront-api-types.js +4 -2
  52. package/dist/esnext/types.d.ts +2 -1
  53. package/dist/esnext/utilities/apiRoutes.d.ts +2 -0
  54. package/dist/esnext/utilities/apiRoutes.js +12 -1
  55. package/dist/esnext/utilities/fetch.js +4 -1
  56. package/dist/esnext/utilities/log/log.js +20 -5
  57. package/dist/esnext/utilities/log/utils.js +1 -1
  58. package/dist/esnext/utilities/template.d.ts +7 -6
  59. package/dist/esnext/utilities/template.js +39 -1
  60. package/dist/esnext/utilities/vite.d.ts +1 -0
  61. package/dist/esnext/utilities/vite.js +4 -0
  62. package/dist/esnext/version.d.ts +1 -1
  63. package/dist/esnext/version.js +1 -1
  64. package/dist/node/foundation/session/session-types.d.ts +2 -0
  65. package/dist/node/framework/plugin.js +6 -5
  66. package/dist/node/framework/plugins/vite-plugin-assets-version.d.ts +2 -0
  67. package/dist/node/framework/plugins/vite-plugin-assets-version.js +15 -0
  68. package/dist/node/framework/plugins/vite-plugin-client-imports.js +6 -3
  69. package/dist/node/framework/plugins/vite-plugin-css-modules-rsc.js +3 -0
  70. package/dist/node/framework/plugins/vite-plugin-css-rsc.js +8 -5
  71. package/dist/node/framework/plugins/vite-plugin-hydration-auto-import.js +7 -1
  72. package/dist/{esnext/framework/plugins/vite-plugin-purge-query-cache.d.ts → node/framework/plugins/vite-plugin-hydrogen-client-components-cache.d.ts} +1 -1
  73. package/dist/node/framework/plugins/{vite-plugin-hydrogen-client-middleware.js → vite-plugin-hydrogen-client-components-cache.js} +2 -2
  74. package/dist/node/framework/plugins/vite-plugin-hydrogen-config.d.ts +2 -1
  75. package/dist/node/framework/plugins/vite-plugin-hydrogen-config.js +107 -90
  76. package/dist/node/framework/plugins/vite-plugin-hydrogen-middleware.js +6 -6
  77. package/dist/node/framework/plugins/vite-plugin-hydrogen-suppress-warnings.js +5 -0
  78. package/dist/node/framework/plugins/vite-plugin-hydrogen-virtual-files.js +45 -7
  79. package/dist/node/framework/plugins/vite-plugin-platform-entry.js +14 -1
  80. package/dist/node/framework/plugins/vite-plugin-ssr-interop.js +5 -1
  81. package/dist/node/framework/types.d.ts +1 -0
  82. package/dist/node/framework/viteception.js +7 -1
  83. package/dist/node/utilities/vite.d.ts +1 -0
  84. package/dist/node/utilities/vite.js +30 -0
  85. package/package.json +7 -6
  86. package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-plugin.js +36 -25
  87. package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-writer.browser.development.server.js +105 -29
  88. package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-writer.browser.production.min.server.js +30 -29
  89. package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-writer.node.development.server.js +92 -29
  90. package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-writer.node.production.min.server.js +31 -29
  91. package/vendor/react-server-dom-vite/esm/react-server-dom-vite-plugin.js +29 -18
  92. package/vendor/react-server-dom-vite/esm/react-server-dom-vite-writer.browser.server.js +105 -29
  93. package/vendor/react-server-dom-vite/esm/react-server-dom-vite-writer.node.server.js +92 -29
  94. package/vendor/react-server-dom-vite/package.json +2 -2
  95. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-client-middleware.d.ts +0 -9
  96. package/dist/esnext/framework/plugins/vite-plugin-purge-query-cache.js +0 -11
  97. package/dist/node/framework/plugins/vite-plugin-hydrogen-client-middleware.d.ts +0 -9
  98. package/dist/node/framework/plugins/vite-plugin-purge-query-cache.js +0 -16
package/README.md CHANGED
@@ -8,7 +8,7 @@ Hydrogen is a React framework and SDK that you can use to build fast and dynamic
8
8
 
9
9
  **Requirements:**
10
10
 
11
- - Node.js version 16.5.0 or higher
11
+ - Node.js version 16.14.0 or higher
12
12
  - Yarn
13
13
 
14
14
  ```bash
@@ -219,6 +219,7 @@ export function CartProvider({ children, numCartLines, onCreate, onLineAdd, onLi
219
219
  ClientAnalytics.publish(ClientAnalytics.eventNames.ADD_TO_CART, true, {
220
220
  addedCartLines: cart.lines,
221
221
  cart: data.cartCreate.cart,
222
+ prevCart: null,
222
223
  });
223
224
  }
224
225
  dispatch({
@@ -258,6 +259,7 @@ export function CartProvider({ children, numCartLines, onCreate, onLineAdd, onLi
258
259
  ClientAnalytics.publish(ClientAnalytics.eventNames.ADD_TO_CART, true, {
259
260
  addedCartLines: lines,
260
261
  cart: data.cartLinesAdd.cart,
262
+ prevCart: state.cart,
261
263
  });
262
264
  dispatch({
263
265
  type: 'resolve',
@@ -289,6 +291,7 @@ export function CartProvider({ children, numCartLines, onCreate, onLineAdd, onLi
289
291
  ClientAnalytics.publish(ClientAnalytics.eventNames.REMOVE_FROM_CART, true, {
290
292
  removedCartLines: lines,
291
293
  cart: data.cartLinesRemove.cart,
294
+ prevCart: state.cart,
292
295
  });
293
296
  dispatch({
294
297
  type: 'resolve',
@@ -320,6 +323,8 @@ export function CartProvider({ children, numCartLines, onCreate, onLineAdd, onLi
320
323
  ClientAnalytics.publish(ClientAnalytics.eventNames.UPDATE_CART, true, {
321
324
  updatedCartLines: lines,
322
325
  oldCart: state.cart,
326
+ cart: data.cartLinesUpdate.cart,
327
+ prevCart: state.cart,
323
328
  });
324
329
  dispatch({
325
330
  type: 'resolve',
@@ -432,6 +437,7 @@ export function CartProvider({ children, numCartLines, onCreate, onLineAdd, onLi
432
437
  ClientAnalytics.publish(ClientAnalytics.eventNames.DISCOUNT_CODE_UPDATED, true, {
433
438
  updatedDiscountCodes: discountCodes,
434
439
  cart: data.cartDiscountCodesUpdate.cart,
440
+ prevCart: state.cart,
435
441
  });
436
442
  dispatch({
437
443
  type: 'resolve',
@@ -7,4 +7,4 @@ export declare const CartBuyerIdentityUpdate: (cartFragment: string) => string;
7
7
  export declare const CartAttributesUpdate: (cartFragment: string) => string;
8
8
  export declare const CartDiscountCodesUpdate: (cartFragment: string) => string;
9
9
  export declare const CartQuery: (cartFragment: string) => string;
10
- export declare const defaultCartFragment = "\nfragment CartFragment on Cart {\n id\n checkoutUrl\n totalQuantity\n buyerIdentity {\n countryCode\n customer {\n id\n email\n firstName\n lastName\n displayName\n }\n email\n phone\n }\n lines(first: $numCartLines) {\n edges {\n node {\n id\n quantity\n attributes {\n key\n value\n }\n cost {\n totalAmount {\n amount\n currencyCode\n }\n compareAtAmountPerQuantity {\n amount\n currencyCode\n }\n }\n merchandise {\n ... on ProductVariant {\n id\n availableForSale\n compareAtPriceV2 {\n ...MoneyFragment\n }\n priceV2 {\n ...MoneyFragment\n }\n requiresShipping\n title\n image {\n ...ImageFragment\n }\n product {\n handle\n title\n }\n selectedOptions {\n name\n value\n }\n }\n }\n }\n }\n }\n cost {\n subtotalAmount {\n ...MoneyFragment\n }\n totalAmount {\n ...MoneyFragment\n }\n totalDutyAmount {\n ...MoneyFragment\n }\n totalTaxAmount {\n ...MoneyFragment\n }\n }\n note\n attributes {\n key\n value\n }\n discountCodes {\n code\n }\n}\n\nfragment MoneyFragment on MoneyV2 {\n currencyCode\n amount\n}\nfragment ImageFragment on Image {\n id\n url\n altText\n width\n height\n}\n";
10
+ export declare const defaultCartFragment = "\nfragment CartFragment on Cart {\n id\n checkoutUrl\n totalQuantity\n buyerIdentity {\n countryCode\n customer {\n id\n email\n firstName\n lastName\n displayName\n }\n email\n phone\n }\n lines(first: $numCartLines) {\n edges {\n node {\n id\n quantity\n attributes {\n key\n value\n }\n cost {\n totalAmount {\n amount\n currencyCode\n }\n compareAtAmountPerQuantity {\n amount\n currencyCode\n }\n }\n merchandise {\n ... on ProductVariant {\n id\n availableForSale\n compareAtPriceV2 {\n ...MoneyFragment\n }\n priceV2 {\n ...MoneyFragment\n }\n requiresShipping\n title\n image {\n ...ImageFragment\n }\n product {\n handle\n title\n id\n }\n selectedOptions {\n name\n value\n }\n }\n }\n }\n }\n }\n cost {\n subtotalAmount {\n ...MoneyFragment\n }\n totalAmount {\n ...MoneyFragment\n }\n totalDutyAmount {\n ...MoneyFragment\n }\n totalTaxAmount {\n ...MoneyFragment\n }\n }\n note\n attributes {\n key\n value\n }\n discountCodes {\n code\n }\n}\n\nfragment MoneyFragment on MoneyV2 {\n currencyCode\n amount\n}\nfragment ImageFragment on Image {\n id\n url\n altText\n width\n height\n}\n";
@@ -154,6 +154,7 @@ fragment CartFragment on Cart {
154
154
  product {
155
155
  handle
156
156
  title
157
+ id
157
158
  }
158
159
  selectedOptions {
159
160
  name
@@ -5,10 +5,10 @@ import { useEmbeddedVideoUrl } from '../../utilities/index.js';
5
5
  * API's [ExternalVideo object](https://shopify.dev/api/storefront/reference/products/externalvideo).
6
6
  */
7
7
  export function ExternalVideo(props) {
8
- const { data, options, id = data.id, frameBorder = '0', allow = 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture', allowFullScreen = true, ...passthroughProps } = props;
8
+ const { data, options, id = data.id, frameBorder = '0', allow = 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture', allowFullScreen = true, loading = 'lazy', ...passthroughProps } = props;
9
9
  if (!data.embedUrl) {
10
10
  throw new Error(`<ExternalVideo/> requires the 'embedUrl' property`);
11
11
  }
12
12
  const url = useEmbeddedVideoUrl(data.embedUrl, options);
13
- return (React.createElement("iframe", { ...passthroughProps, id: id ?? data.embedUrl, frameBorder: frameBorder, allow: allow, allowFullScreen: allowFullScreen, src: url }));
13
+ return (React.createElement("iframe", { ...passthroughProps, id: id ?? data.embedUrl, frameBorder: frameBorder, allow: allow, allowFullScreen: allowFullScreen, src: url, loading: loading }));
14
14
  }
@@ -79,7 +79,7 @@ declare type LoaderProps<GenericLoaderOpts> = {
79
79
  */
80
80
  loaderOptions?: GenericLoaderOpts;
81
81
  };
82
- declare type ExternalImageProps<GenericLoaderOpts> = SetRequired<HtmlImageProps, 'src' | 'width' | 'height' | 'alt'> & {
82
+ export declare type ExternalImageProps<GenericLoaderOpts> = SetRequired<HtmlImageProps, 'src' | 'width' | 'height' | 'alt'> & {
83
83
  /** A custom function that generates the image URL. Parameters passed in
84
84
  * are either `ShopifyLoaderParams` if using the `data` prop, or the
85
85
  * `LoaderOptions` object that you pass to `loaderOptions`.
@@ -27,7 +27,7 @@ export function Image(props) {
27
27
  return React.createElement(ExternalImage, { ...props });
28
28
  }
29
29
  }
30
- function ShopifyImage({ data, width, height, loading, loader = shopifyImageLoader, loaderOptions, widths, ...rest }) {
30
+ function ShopifyImage({ data, width, height, loading, loader = shopifyImageLoader, loaderOptions, widths, decoding = 'async', ...rest }) {
31
31
  if (!data.url) {
32
32
  throw new Error(`<Image/>: the 'data' prop requires the 'url' property`);
33
33
  }
@@ -72,10 +72,10 @@ function ShopifyImage({ data, width, height, loading, loader = shopifyImageLoade
72
72
  loader,
73
73
  });
74
74
  /* eslint-disable hydrogen/prefer-image-component */
75
- return (React.createElement("img", { id: data.id ?? '', alt: data.altText ?? rest.alt ?? '', loading: loading ?? 'lazy', ...rest, src: finalSrc, width: imgElementWidth ?? undefined, height: imgElementHeight ?? undefined, srcSet: finalSrcset }));
75
+ return (React.createElement("img", { id: data.id ?? '', alt: data.altText ?? rest.alt ?? '', loading: loading ?? 'lazy', ...rest, src: finalSrc, width: imgElementWidth ?? undefined, height: imgElementHeight ?? undefined, srcSet: finalSrcset, decoding: decoding }));
76
76
  /* eslint-enable hydrogen/prefer-image-component */
77
77
  }
78
- function ExternalImage({ src, width, height, alt, loader, loaderOptions, widths, loading, ...rest }) {
78
+ function ExternalImage({ src, width, height, alt, loader, loaderOptions, widths, loading, decoding = 'async', ...rest }) {
79
79
  if (!width || !height) {
80
80
  throw new Error(`<Image/>: when 'src' is provided, 'width' and 'height' are required and need to be valid values (i.e. greater than zero). Provided values: 'src': ${src}, 'width': ${width}, 'height': ${height}`);
81
81
  }
@@ -112,7 +112,7 @@ function ExternalImage({ src, width, height, alt, loader, loaderOptions, widths,
112
112
  // @ts-expect-error TS doesn't understand that it could exist
113
113
  width: loaderOptions?.width ?? width,
114
114
  // @ts-expect-error TS doesn't understand that it could exist
115
- height: loaderOptions?.height ?? height, alt: alt ?? '', loading: loading ?? 'lazy', srcSet: finalSrcset }));
115
+ height: loaderOptions?.height ?? height, alt: alt ?? '', loading: loading ?? 'lazy', srcSet: finalSrcset, decoding: decoding }));
116
116
  /* eslint-enable hydrogen/prefer-image-component */
117
117
  }
118
118
  function internalImageSrcSet({ src, width, crop, scale, widths, loader, height, }) {
@@ -1,2 +1,2 @@
1
1
  export { Image } from './Image.js';
2
- export type { ShopifyLoaderParams, ShopifyLoaderOptions, ShopifyImageProps, } from './Image.js';
2
+ export type { ShopifyLoaderParams, ShopifyLoaderOptions, ShopifyImageProps, ExternalImageProps, } from './Image.js';
@@ -1,7 +1,7 @@
1
1
  export { Link } from './Link/index.js';
2
2
  export { MediaFile } from './MediaFile/index.js';
3
3
  export { Video } from './Video/index.js';
4
- export { Image } from './Image/index.js';
4
+ export { Image, type ExternalImageProps, type ShopifyImageProps, } from './Image/index.js';
5
5
  export { ExternalVideo } from './ExternalVideo/index.js';
6
6
  export { AddToCartButton } from './AddToCartButton/index.js';
7
7
  export { ModelViewer } from './ModelViewer/index.js';
@@ -1,7 +1,7 @@
1
1
  export { Link } from './Link/index.js';
2
2
  export { MediaFile } from './MediaFile/index.js';
3
3
  export { Video } from './Video/index.js';
4
- export { Image } from './Image/index.js';
4
+ export { Image, } from './Image/index.js';
5
5
  export { ExternalVideo } from './ExternalVideo/index.js';
6
6
  export { AddToCartButton } from './AddToCartButton/index.js';
7
7
  export { ModelViewer } from './ModelViewer/index.js';
@@ -4,6 +4,7 @@ export declare const EVENT_PATHNAME_REGEX: RegExp;
4
4
  export declare const OXYGEN_SECRET_TOKEN_ENVIRONMENT_VARIABLE = "SHOPIFY_STOREFRONT_API_SECRET_TOKEN";
5
5
  export declare const STOREFRONT_API_SECRET_TOKEN_HEADER = "Shopify-Storefront-Private-Token";
6
6
  export declare const STOREFRONT_API_PUBLIC_TOKEN_HEADER = "X-Shopify-Storefront-Access-Token";
7
+ export declare const FORM_REDIRECT_COOKIE = "Hydrogen-Redirect";
7
8
  export declare const STOREFRONT_API_BUYER_IP_HEADER = "Shopify-Storefront-Buyer-IP";
8
9
  export declare const SHOPIFY_STOREFRONT_ID_VARIABLE = "SHOPIFY_STOREFRONT_ID";
9
10
  export declare const SHOPIFY_STOREFRONT_ID_HEADER = "Shopify-Storefront-Id";
@@ -4,6 +4,7 @@ export const EVENT_PATHNAME_REGEX = new RegExp(`^${EVENT_PATHNAME}/`);
4
4
  export const OXYGEN_SECRET_TOKEN_ENVIRONMENT_VARIABLE = 'SHOPIFY_STOREFRONT_API_SECRET_TOKEN';
5
5
  export const STOREFRONT_API_SECRET_TOKEN_HEADER = 'Shopify-Storefront-Private-Token';
6
6
  export const STOREFRONT_API_PUBLIC_TOKEN_HEADER = 'X-Shopify-Storefront-Access-Token';
7
+ export const FORM_REDIRECT_COOKIE = 'Hydrogen-Redirect';
7
8
  export const STOREFRONT_API_BUYER_IP_HEADER = 'Shopify-Storefront-Buyer-IP';
8
9
  export const SHOPIFY_STOREFRONT_ID_VARIABLE = 'SHOPIFY_STOREFRONT_ID';
9
10
  export const SHOPIFY_STOREFRONT_ID_HEADER = 'Shopify-Storefront-Id';
@@ -1,6 +1,6 @@
1
1
  import React, { Suspense, useState, StrictMode, Fragment, useEffect, } from 'react';
2
2
  import { hydrateRoot } from 'react-dom/client';
3
- import { ErrorBoundary } from 'react-error-boundary';
3
+ import { ErrorBoundary } from 'react-error-boundary/dist/react-error-boundary.esm';
4
4
  import { createFromFetch, createFromReadableStream,
5
5
  // @ts-ignore
6
6
  } from '@shopify/hydrogen/vendor/react-server-dom-vite';
@@ -10,7 +10,7 @@ import { ServerPropsProvider } from './foundation/ServerPropsProvider/index.js';
10
10
  import { isBotUA } from './utilities/bot-ua.js';
11
11
  import { getCache, setCache } from './foundation/runtime.js';
12
12
  import { ssrRenderToPipeableStream, ssrRenderToReadableStream, rscRenderToReadableStream, createFromReadableStream, bufferReadableStream, } from './streaming.server.js';
13
- import { stripScriptsFromTemplate } from './utilities/template.js';
13
+ import { getTemplate } from './utilities/template.js';
14
14
  import { Analytics } from './foundation/Analytics/Analytics.server.js';
15
15
  import { DevTools } from './foundation/DevTools/DevTools.server.js';
16
16
  import { getSyncSessionApi } from './foundation/session/session.js';
@@ -20,6 +20,14 @@ import { splitCookiesString } from 'set-cookie-parser';
20
20
  import { deleteItemFromCache, getItemFromCache, isStale, setItemInCache, } from './foundation/Cache/cache.js';
21
21
  import { CacheShort, NO_STORE } from './foundation/Cache/strategies/index.js';
22
22
  import { getBuiltInRoute } from './foundation/BuiltInRoutes/BuiltInRoutes.js';
23
+ import { FORM_REDIRECT_COOKIE } from './constants.js';
24
+ // Importing 'stream' directly breaks Vite resolve
25
+ // when building for workers, even though this code
26
+ // does not run in a worker. Looks like tree-shaking
27
+ // kicks in after the import analysis/bundle.
28
+ // @ts-ignore
29
+ import stream from 'virtual__stream';
30
+ const PassThrough = stream.PassThrough;
23
31
  const DOCTYPE = '<!DOCTYPE html>';
24
32
  const CONTENT_TYPE = 'Content-Type';
25
33
  const HTML_CONTENT_TYPE = 'text/html; charset=UTF-8';
@@ -29,9 +37,10 @@ export const renderHydrogen = (App) => {
29
37
  const request = new HydrogenRequest(rawRequest);
30
38
  const url = new URL(request.url);
31
39
  let sessionApi = options.sessionApi;
32
- const { default: inlineHydrogenConfig } = await import(
40
+ const { default: importedConfig } = await import(
33
41
  // @ts-ignore
34
42
  'virtual__hydrogen.config.ts');
43
+ const inlineHydrogenConfig = typeof importedConfig === 'function' ? importedConfig() : importedConfig;
35
44
  const { default: hydrogenRoutes } = await import(
36
45
  // @ts-ignore
37
46
  'virtual__hydrogen-routes.server.jsx');
@@ -43,9 +52,13 @@ export const renderHydrogen = (App) => {
43
52
  request.ctx.buyerIpHeader = buyerIpHeader;
44
53
  setLogger(hydrogenConfig.logger);
45
54
  const log = getLoggerWithContext(request);
46
- const response = new HydrogenResponse(null, {
55
+ const response = new HydrogenResponse(request.url, null, {
47
56
  headers: headers || {},
48
57
  });
58
+ if (request.cookies.get(FORM_REDIRECT_COOKIE)) {
59
+ response.headers.set('SET-COOKIE', `${FORM_REDIRECT_COOKIE}=`);
60
+ response.doNotStream();
61
+ }
49
62
  if (hydrogenConfig.poweredByHeader ?? true) {
50
63
  // If undefined in the config, then always show the header
51
64
  response.headers.set('powered-by', 'Shopify-Hydrogen');
@@ -91,18 +104,21 @@ export const renderHydrogen = (App) => {
91
104
  maxAge: 10,
92
105
  }));
93
106
  await processRequest(handleRequest, App, url, request, sessionApi, options, response, hydrogenConfig, true);
107
+ response.markAsSent();
94
108
  }
95
109
  catch (e) {
96
110
  log.error('Cache revalidate error', e);
97
111
  }
98
112
  });
99
113
  // Asynchronously wait for it in workers
100
- request.ctx.runtime?.waitUntil(staleWhileRevalidatePromise);
114
+ request.ctx.runtime?.waitUntil?.(staleWhileRevalidatePromise);
101
115
  }
102
116
  return cachedResponse;
103
117
  }
104
118
  }
105
- return processRequest(handleRequest, App, url, request, sessionApi, options, response, hydrogenConfig);
119
+ const result = await processRequest(handleRequest, App, url, request, sessionApi, options, response, hydrogenConfig);
120
+ response.markAsSent();
121
+ return result;
106
122
  };
107
123
  if (__HYDROGEN_WORKER__)
108
124
  return handleRequest;
@@ -137,6 +153,7 @@ async function processRequest(handleRequest, App, url, request, sessionApi, opti
137
153
  if (isRSCRequest) {
138
154
  const buffered = await bufferReadableStream(rsc.readable.getReader());
139
155
  postRequestTasks('rsc', 200, request, response);
156
+ response.headers.set('cache-control', response.cacheControlHeader);
140
157
  cacheResponse(response, request, [buffered], revalidate);
141
158
  return new Response(buffered, {
142
159
  headers: response.headers,
@@ -154,19 +171,10 @@ async function processRequest(handleRequest, App, url, request, sessionApi, opti
154
171
  request,
155
172
  response,
156
173
  nodeResponse,
157
- template: await getTemplate(indexTemplate, url),
158
174
  revalidate,
175
+ template: await getTemplate(indexTemplate, url),
159
176
  });
160
177
  }
161
- async function getTemplate(indexTemplate, url) {
162
- let template = typeof indexTemplate === 'function'
163
- ? await indexTemplate(url.toString())
164
- : indexTemplate;
165
- if (template && typeof template !== 'string') {
166
- template = template.default;
167
- }
168
- return template;
169
- }
170
178
  function getApiRoute(url, routes) {
171
179
  const apiRoutes = getApiRoutes(routes);
172
180
  return getApiRouteFromURL(url, apiRoutes);
@@ -185,18 +193,21 @@ function assembleHtml({ ssrHtml, rscPayload, request, template, }) {
185
193
  * Run the SSR/Fizz part of the App. If streaming is disabled,
186
194
  * this buffers the output and applies SEO enhancements.
187
195
  */
188
- async function runSSR({ rsc, state, request, response, nodeResponse, template, nonce, dev, log, revalidate, }) {
196
+ async function runSSR({ rsc, state, request, response, nodeResponse, nonce, dev, log, revalidate, template: { raw: template, noScriptTemplate, bootstrapModules, bootstrapScripts, linkHeader, }, }) {
189
197
  let ssrDidError;
190
198
  const didError = () => rsc.didError() ?? ssrDidError;
191
199
  const [rscReadableForFizz, rscReadableForFlight] = rsc.readable.tee();
192
200
  const rscResponse = createFromReadableStream(rscReadableForFizz);
193
201
  const RscConsumer = () => rscResponse.readRoot();
194
- const { noScriptTemplate, bootstrapScripts, bootstrapModules } = stripScriptsFromTemplate(template);
195
202
  const AppSSR = (React.createElement(Html, { template: response.canStream() ? noScriptTemplate : template, hydrogenConfig: request.ctx.hydrogenConfig },
196
203
  React.createElement(ServerRequestProvider, { request: request },
197
204
  React.createElement(ServerPropsProvider, { initialServerProps: state, setServerPropsForRsc: () => { }, setRscResponseFromApiRoute: () => { } },
198
205
  React.createElement(Suspense, { fallback: null },
199
206
  React.createElement(RscConsumer, null))))));
207
+ if (linkHeader) {
208
+ const existingLinkHeader = response.headers.get('Link');
209
+ response.headers.set('Link', existingLinkHeader ? existingLinkHeader + ', ' + linkHeader : linkHeader);
210
+ }
200
211
  log.trace('start ssr');
201
212
  const rscReadable = response.canStream()
202
213
  ? new ReadableStream({
@@ -373,7 +384,7 @@ async function runSSR({ rsc, state, request, response, nodeResponse, template, n
373
384
  // Redirects found after any async code
374
385
  return nodeResponse.end();
375
386
  }
376
- const bufferedResponse = await createNodeWriter();
387
+ const bufferedResponse = new PassThrough();
377
388
  const bufferedRscPromise = bufferReadableStream(rscReadable.getReader());
378
389
  let ssrHtml = '';
379
390
  bufferedResponse.on('data', (chunk) => (ssrHtml += chunk.toString()));
@@ -492,15 +503,6 @@ function isRedirect(response) {
492
503
  const status = response.status ?? response.statusCode ?? 0;
493
504
  return status >= 300 && status < 400;
494
505
  }
495
- async function createNodeWriter() {
496
- // Importing 'stream' directly breaks Vite resolve
497
- // when building for workers, even though this code
498
- // does not run in a worker. Looks like tree-shaking
499
- // kicks in after the import analysis/bundle.
500
- const streamImport = __HYDROGEN_WORKER__ ? '' : 'stream';
501
- const { PassThrough } = await import(streamImport);
502
- return new PassThrough();
503
- }
504
506
  function flightContainer(chunk) {
505
507
  return `<meta data-flight="${htmlEncode(chunk)}" />`;
506
508
  }
@@ -593,7 +595,7 @@ async function cacheResponse(response, request, chunks, revalidate) {
593
595
  }
594
596
  else {
595
597
  const cachePutPromise = Promise.resolve(true).then(() => saveCacheResponse(response, request, chunks));
596
- request.ctx.runtime?.waitUntil(cachePutPromise);
598
+ request.ctx.runtime?.waitUntil?.(cachePutPromise);
597
599
  }
598
600
  }
599
601
  }
@@ -1 +1,2 @@
1
1
  export { Form } from './foundation/Form/Form.client.js';
2
+ export { useFlashSession } from './foundation/useSession/useSession.js';
@@ -1 +1,2 @@
1
1
  export { Form } from './foundation/Form/Form.client.js';
2
+ export { useFlashSession } from './foundation/useSession/useSession.js';
@@ -88,7 +88,6 @@ export async function setItemInCache(request, response, userCacheOptions) {
88
88
  const cacheControlString = generateDefaultCacheControlHeader(getCacheControlSetting(cacheControl));
89
89
  // CF will override cache-control, so we need to keep a
90
90
  // non-modified real-cache-control
91
- response.headers.set('cache-control', cacheControlString);
92
91
  response.headers.set('real-cache-control', cacheControlString);
93
92
  response.headers.set('cache-put-date', new Date().toUTCString());
94
93
  logCacheApiStatus('PUT', request.url);
@@ -46,6 +46,7 @@ export declare class HydrogenRequest extends Request {
46
46
  router: RouterContextData;
47
47
  buyerIpHeader?: string;
48
48
  session?: SessionSyncApi;
49
+ flashSession: Record<string, any>;
49
50
  runtime?: RuntimeContext;
50
51
  scopes: Map<string, Record<string, any>>;
51
52
  localization?: LocalizationContextValue;
@@ -60,6 +60,7 @@ export class HydrogenRequest extends Request {
60
60
  },
61
61
  preloadQueries: new Map(),
62
62
  scopes: new Map(),
63
+ flashSession: {},
63
64
  };
64
65
  this.cookies = this.parseCookies();
65
66
  }
@@ -2,11 +2,13 @@ import type { CachingStrategy } from '../../types.js';
2
2
  import React from 'react';
3
3
  export declare class HydrogenResponse extends Response {
4
4
  private wait;
5
+ private sent;
5
6
  private cacheOptions;
6
- private proxy;
7
7
  status: number;
8
8
  statusText: string;
9
- constructor(...args: ConstructorParameters<typeof Response>);
9
+ url: string;
10
+ constructor(url: string, body: any, init: any);
11
+ markAsSent(): void;
10
12
  /**
11
13
  * Buffer the current response until all queries have resolved,
12
14
  * and prevent it from streaming back early.
@@ -1,31 +1,32 @@
1
1
  import { CacheShort, generateCacheControlHeader, } from '../Cache/strategies/index.js';
2
2
  import Redirect from '../Redirect/Redirect.client.js';
3
3
  import React from 'react';
4
+ import { log } from '../../utilities/log/log.js';
4
5
  export class HydrogenResponse extends Response {
5
6
  wait = false;
7
+ sent = false;
6
8
  cacheOptions = CacheShort();
7
- proxy = Object.defineProperties(Object.create(null), {
8
- // Default values:
9
- status: { value: 200, writable: true },
10
- statusText: { value: '', writable: true },
11
- });
12
- // @ts-ignore
13
- status;
14
- // @ts-ignore
15
- statusText;
16
- constructor(...args) {
17
- super(...args);
18
- return new Proxy(this, {
19
- get: (target, key) => target.proxy[key] ?? Reflect.get(target, key),
20
- set: (target, key, value) => Reflect.set(key in target.proxy ? target.proxy : target, key, value),
21
- });
9
+ status = 200;
10
+ statusText = '';
11
+ url;
12
+ constructor(url, body, init) {
13
+ super(body, init);
14
+ this.url = url;
15
+ }
16
+ markAsSent() {
17
+ this.sent = true;
22
18
  }
23
19
  /**
24
20
  * Buffer the current response until all queries have resolved,
25
21
  * and prevent it from streaming back early.
26
22
  */
27
23
  doNotStream() {
28
- this.wait = true;
24
+ if (!this.sent) {
25
+ this.wait = true;
26
+ }
27
+ else {
28
+ log.warn(`response.doNotStream() failed, the stream has already started on: ${this.url}\nDisabling streaming should always be the first thing in your route server component.`);
29
+ }
29
30
  }
30
31
  canStream() {
31
32
  return !this.wait;
@@ -1,6 +1,4 @@
1
- import React, { createContext, useMemo, useCallback,
2
- // @ts-ignore
3
- useTransition, useState, } from 'react';
1
+ import React, { createContext, useMemo, useCallback, useTransition, useState, } from 'react';
4
2
  const PRIVATE_PROPS = ['request', 'response'];
5
3
  export const ServerPropsContext = createContext(null);
6
4
  export function ServerPropsProvider({ initialServerProps, setServerPropsForRsc, setRscResponseFromApiRoute, children, }) {
@@ -1,7 +1,8 @@
1
- declare type ResponseSyncInit = [string, ResponseInit];
1
+ declare type ResponseSyncInit = [string, ResponseInit, string];
2
2
  export declare class ResponseSync extends Response {
3
3
  #private;
4
4
  bodyUsed: boolean;
5
+ url: string;
5
6
  constructor(init: ResponseSyncInit);
6
7
  text(): string;
7
8
  json(): any;
@@ -4,16 +4,27 @@ export class ResponseSync extends Response {
4
4
  bodyUsed = true;
5
5
  #text;
6
6
  #json;
7
+ url;
7
8
  constructor(init) {
8
- super(...init);
9
+ super(init[0], init[1]);
9
10
  this.#text = init[0];
11
+ this.url = init[2];
10
12
  }
11
13
  // @ts-expect-error Changing inherited types
12
14
  text() {
13
15
  return this.#text;
14
16
  }
15
17
  json() {
16
- return (this.#json ??= parseJSON(this.#text));
18
+ try {
19
+ return (this.#json ??= parseJSON(this.#text));
20
+ }
21
+ catch (e) {
22
+ if (!this.ok) {
23
+ throw new Error(`Request to ${this.url} failed with ${this.status} and the response body is not parseable.\nMake sure to handle the error state when using fetchSync.`);
24
+ }
25
+ else
26
+ throw e;
27
+ }
17
28
  }
18
29
  /**
19
30
  * @deprecated Access response properties at the top level instead.
@@ -33,6 +44,7 @@ export class ResponseSync extends Response {
33
44
  statusText: response.statusText,
34
45
  headers: Array.from(response.headers.entries()),
35
46
  },
47
+ response.url,
36
48
  ];
37
49
  }
38
50
  }
@@ -1,10 +1,12 @@
1
1
  export declare type SessionSyncApi = {
2
2
  get: () => Record<string, string>;
3
+ set: (data: Record<string, any>) => any;
3
4
  };
4
5
  export declare type SessionApi = {
5
6
  get: () => Promise<Record<string, string>>;
6
7
  set: (key: string, value: string) => Promise<void>;
7
8
  destroy: () => Promise<void>;
9
+ getFlash: (key: string) => any;
8
10
  };
9
11
  export declare type SessionStorageAdapter = {
10
12
  get: (request: Request) => Promise<Record<string, string>>;
@@ -4,12 +4,15 @@ import type { HydrogenRequest } from '../HydrogenRequest/HydrogenRequest.server.
4
4
  import type { SessionStorageAdapter } from './session-types.js';
5
5
  export declare function getSyncSessionApi(request: HydrogenRequest, componentResponse: HydrogenResponse, log: Logger, session?: SessionStorageAdapter): {
6
6
  get(): any;
7
+ set(data: Record<string, any>): any;
7
8
  };
8
9
  export declare const emptySessionImplementation: (log: Logger) => {
10
+ getFlash(key: string): Promise<null>;
9
11
  get(): Promise<{}>;
10
12
  set(key: string, value: string): Promise<void>;
11
13
  destroy(): Promise<void>;
12
14
  };
13
15
  export declare const emptySyncSessionImplementation: (log: Logger) => {
14
16
  get(): {};
17
+ set(data: Record<string, any>): null;
15
18
  };
@@ -9,11 +9,23 @@ export function getSyncSessionApi(request, componentResponse, log, session) {
9
9
  }
10
10
  return sessionPromises.getPromise.read();
11
11
  },
12
+ set(data) {
13
+ if (!sessionPromises.setPromise) {
14
+ sessionPromises.setPromise = wrapPromise(session.set(request, data));
15
+ }
16
+ const cookie = sessionPromises.setPromise.read();
17
+ componentResponse.headers.set('Set-Cookie', cookie);
18
+ return cookie;
19
+ },
12
20
  }
13
21
  : emptySyncSessionImplementation(log);
14
22
  }
15
23
  export const emptySessionImplementation = function (log) {
16
24
  return {
25
+ async getFlash(key) {
26
+ log.warn('No session adapter has been configured!');
27
+ return null;
28
+ },
17
29
  async get() {
18
30
  log.warn('No session adapter has been configured!');
19
31
  return {};
@@ -33,5 +45,9 @@ export const emptySyncSessionImplementation = function (log) {
33
45
  log.warn('No session adapter has been configured!');
34
46
  return {};
35
47
  },
48
+ set(data) {
49
+ log.warn('No session adapter has been configured!');
50
+ return null;
51
+ },
36
52
  };
37
53
  };
@@ -13,6 +13,9 @@ export interface HydrogenUseQueryOptions {
13
13
  */
14
14
  shouldCacheResponse?: (body: any) => boolean;
15
15
  }
16
+ declare global {
17
+ var __HYDROGEN_CACHE_ID__: string;
18
+ }
16
19
  /**
17
20
  * The `useQuery` hook executes an asynchronous operation like `fetch` in a way that
18
21
  * supports [Suspense](https://reactjs.org/docs/concurrent-mode-suspense.html). You can use this
@@ -19,7 +19,7 @@ queryFn,
19
19
  queryOptions) {
20
20
  const request = useServerRequest();
21
21
  const withCacheIdKey = [
22
- '__QUERY_CACHE_ID__',
22
+ __HYDROGEN_CACHE_ID__,
23
23
  ...(typeof key === 'string' ? [key] : key),
24
24
  ];
25
25
  const fetcher = cachedQueryFnBuilder(withCacheIdKey, queryFn, queryOptions);
@@ -1,2 +1,3 @@
1
1
  /** The `useSession` hook reads session data in server components. */
2
2
  export declare const useSession: () => Record<string, string>;
3
+ export declare const useFlashSession: (key: string) => string;
@@ -5,3 +5,16 @@ export const useSession = function () {
5
5
  const session = request.ctx.session?.get() || {};
6
6
  return session;
7
7
  };
8
+ export const useFlashSession = function (key) {
9
+ const request = useServerRequest();
10
+ const data = request.ctx.session?.get() || {};
11
+ let value = data[key];
12
+ if (value) {
13
+ delete data[key];
14
+ request.ctx.flashSession[key] = value;
15
+ }
16
+ request.ctx.session?.set(data);
17
+ value = request.ctx.flashSession[key];
18
+ delete request.ctx.flashSession[key];
19
+ return value;
20
+ };