@shopify/hydrogen 0.20.0 → 0.21.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 (78) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/esnext/components/CartEstimatedCost/CartEstimatedCost.client.d.ts +1 -1
  3. package/dist/esnext/components/CartLinePrice/CartLinePrice.client.d.ts +1 -1
  4. package/dist/esnext/components/Money/Money.client.d.ts +11 -5
  5. package/dist/esnext/components/Money/Money.client.js +16 -3
  6. package/dist/esnext/components/ProductPrice/ProductPrice.client.d.ts +1 -2
  7. package/dist/esnext/components/ProductPrice/ProductPrice.client.js +1 -2
  8. package/dist/esnext/components/index.d.ts +0 -3
  9. package/dist/esnext/components/index.js +0 -3
  10. package/dist/esnext/entry-server.js +29 -31
  11. package/dist/esnext/foundation/ServerRequestProvider/ServerRequestProvider.js +18 -3
  12. package/dist/esnext/foundation/useQuery/hooks.js +8 -9
  13. package/dist/esnext/framework/Hydration/ServerComponentResponse.server.d.ts +1 -1
  14. package/dist/esnext/framework/Hydration/ServerComponentResponse.server.js +2 -1
  15. package/dist/esnext/framework/Hydration/rsc.js +55 -7
  16. package/dist/esnext/framework/cache/in-memory.js +0 -6
  17. package/dist/esnext/framework/cache-sub-request.d.ts +17 -0
  18. package/dist/esnext/framework/cache-sub-request.js +64 -0
  19. package/dist/esnext/framework/cache.d.ts +6 -6
  20. package/dist/esnext/framework/cache.js +36 -33
  21. package/dist/esnext/framework/plugin.js +2 -0
  22. package/dist/esnext/framework/plugins/vite-plugin-client-imports.d.ts +2 -0
  23. package/dist/esnext/framework/plugins/vite-plugin-client-imports.js +25 -0
  24. package/dist/esnext/framework/plugins/vite-plugin-css-modules-rsc.js +8 -3
  25. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-config.js +1 -0
  26. package/dist/esnext/index.d.ts +4 -0
  27. package/dist/esnext/index.js +4 -0
  28. package/dist/esnext/types.d.ts +1 -0
  29. package/dist/esnext/utilities/html-encoding.d.ts +2 -0
  30. package/dist/esnext/utilities/html-encoding.js +16 -0
  31. package/dist/esnext/utilities/index.d.ts +1 -0
  32. package/dist/esnext/utilities/index.js +1 -0
  33. package/dist/esnext/utilities/log/log-cache-api-status.js +5 -1
  34. package/dist/esnext/version.d.ts +1 -1
  35. package/dist/esnext/version.js +1 -1
  36. package/dist/node/entry-server.js +29 -31
  37. package/dist/node/foundation/ServerRequestProvider/ServerRequestProvider.js +18 -3
  38. package/dist/node/framework/Hydration/ServerComponentResponse.server.d.ts +1 -1
  39. package/dist/node/framework/Hydration/ServerComponentResponse.server.js +2 -1
  40. package/dist/node/framework/Hydration/rsc.js +55 -7
  41. package/dist/node/framework/cache/in-memory.js +0 -6
  42. package/dist/node/framework/cache-sub-request.d.ts +17 -0
  43. package/dist/node/framework/cache-sub-request.js +95 -0
  44. package/dist/node/framework/cache.d.ts +6 -6
  45. package/dist/node/framework/cache.js +38 -35
  46. package/dist/node/framework/plugin.js +2 -0
  47. package/dist/node/framework/plugins/vite-plugin-client-imports.d.ts +2 -0
  48. package/dist/node/framework/plugins/vite-plugin-client-imports.js +28 -0
  49. package/dist/node/framework/plugins/vite-plugin-css-modules-rsc.js +8 -3
  50. package/dist/node/framework/plugins/vite-plugin-hydrogen-config.js +1 -0
  51. package/dist/node/types.d.ts +1 -0
  52. package/dist/node/utilities/html-encoding.d.ts +2 -0
  53. package/dist/node/utilities/html-encoding.js +21 -0
  54. package/dist/node/utilities/index.d.ts +1 -0
  55. package/dist/node/utilities/index.js +4 -1
  56. package/dist/node/utilities/log/log-cache-api-status.js +5 -1
  57. package/dist/node/version.d.ts +1 -1
  58. package/dist/node/version.js +1 -1
  59. package/package.json +3 -3
  60. package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-plugin.js +29 -4
  61. package/vendor/react-server-dom-vite/esm/react-server-dom-vite-plugin.js +29 -4
  62. package/vendor/react-server-dom-vite/package.json +2 -1
  63. package/dist/esnext/components/ProductDescription/ProductDescription.client.d.ts +0 -13
  64. package/dist/esnext/components/ProductDescription/ProductDescription.client.js +0 -16
  65. package/dist/esnext/components/ProductDescription/index.d.ts +0 -1
  66. package/dist/esnext/components/ProductDescription/index.js +0 -1
  67. package/dist/esnext/components/ProductMetafield/ProductMetafield.client.d.ts +0 -21
  68. package/dist/esnext/components/ProductMetafield/ProductMetafield.client.js +0 -42
  69. package/dist/esnext/components/ProductMetafield/index.d.ts +0 -2
  70. package/dist/esnext/components/ProductMetafield/index.js +0 -1
  71. package/dist/esnext/components/ProductTitle/ProductTitle.client.d.ts +0 -13
  72. package/dist/esnext/components/ProductTitle/ProductTitle.client.js +0 -16
  73. package/dist/esnext/components/ProductTitle/index.d.ts +0 -1
  74. package/dist/esnext/components/ProductTitle/index.js +0 -1
  75. package/dist/esnext/components/UnitPrice/UnitPrice.client.d.ts +0 -15
  76. package/dist/esnext/components/UnitPrice/UnitPrice.client.js +0 -22
  77. package/dist/esnext/components/UnitPrice/index.d.ts +0 -1
  78. package/dist/esnext/components/UnitPrice/index.js +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,59 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.21.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1327](https://github.com/Shopify/hydrogen/pull/1327) [`ce56311f`](https://github.com/Shopify/hydrogen/commit/ce56311fc1b63df22f77b199980439548f76997a) Thanks [@frehner](https://github.com/frehner)! - **Breaking Change**: `<Money />` updates and `<UnitPrice />` Removed.
8
+
9
+ - `<UnitPrice/>` has been removed
10
+ - `<Money/>` has two new props: `measurement` and `measurementSeparator` which do the work that `UnitPrice` used to do
11
+ - The TypeScript types for `<Money/>` have been improved and should provide a better typed experience now
12
+
13
+ * [#1216](https://github.com/Shopify/hydrogen/pull/1216) [`771786a6`](https://github.com/Shopify/hydrogen/commit/771786a6475c4caadb1abe5f6644e2b5c2abc021) Thanks [@wizardlyhel](https://github.com/wizardlyhel)! - Fixes an issue where cached sub-requests were not revalidating properly.
14
+
15
+ - [#1304](https://github.com/Shopify/hydrogen/pull/1304) [`aa196150`](https://github.com/Shopify/hydrogen/commit/aa19615024de4fe16d548429665a863e9aae0248) Thanks [@frehner](https://github.com/frehner)! - Removed `<ProductTitle/>` and `<ProductDescription/>` components. To migrate, use `{product.title}` and `{product.description}` instead.
16
+
17
+ * [#1335](https://github.com/Shopify/hydrogen/pull/1335) [`0d90f92b`](https://github.com/Shopify/hydrogen/commit/0d90f92b448b0c4d99be3e5f5fa25d0b70a8315e) Thanks [@blittle](https://github.com/blittle)! - **Breaking Change**
18
+
19
+ The `<ProductMetafield />` component has been removed. Instead, directly use the `<Metafield>` component.
20
+
21
+ ### Patch Changes
22
+
23
+ - [#1311](https://github.com/Shopify/hydrogen/pull/1311) [`3e3fd72f`](https://github.com/Shopify/hydrogen/commit/3e3fd72f7016c0993deceefc121306cf957ef564) Thanks [@jplhomer](https://github.com/jplhomer)! - Client components no longer need to use `@shopify/hydrogen/client` as the import path. All Hydrogen components can now be imported from `@shopify/hydrogen` regardless of their context.
24
+
25
+ * [#1259](https://github.com/Shopify/hydrogen/pull/1259) [`110e9aca`](https://github.com/Shopify/hydrogen/commit/110e9aca385d553e3a87fea406f8bd8a43a0788f) Thanks [@blittle](https://github.com/blittle)! - You can now easily disable streaming on any page conditionally with the `enableStreaming` option inside `hydrogen.config.js`:
26
+
27
+ ```ts
28
+ import {CookieSessionStorage} from '@shopify/hydrogen';
29
+ import {defineConfig} from '@shopify/hydrogen/config';
30
+
31
+ export default defineConfig({
32
+ routes: import.meta.globEager('./src/routes/**/*.server.[jt](s|sx)'),
33
+ shopify: {
34
+ defaultLocale: 'en-us',
35
+ storeDomain: 'hydrogen-preview.myshopify.com',
36
+ storefrontToken: '3b580e70970c4528da70c98e097c2fa0',
37
+ storefrontApiVersion: '2022-07',
38
+ },
39
+ enableStreaming: (req) => req.headers.get('user-agent') !== 'custom bot',
40
+ });
41
+ ```
42
+
43
+ By default all pages are stream rendered except for SEO bots. There shouldn't be many reasons to disable streaming, unless there is a custom bot not covered by Hydrogen's bot detection.
44
+
45
+ - [#1318](https://github.com/Shopify/hydrogen/pull/1318) [`668a24da`](https://github.com/Shopify/hydrogen/commit/668a24daebf180747a002c8020c2e712f5d9a458) Thanks [@blittle](https://github.com/blittle)! - Buffer RSC flight responses. There isn't any benefit to streaming them, because we start a transition on page navigation. Buffering also fixes caching problems on the flight response.
46
+
47
+ * [#1293](https://github.com/Shopify/hydrogen/pull/1293) [`e378ed61`](https://github.com/Shopify/hydrogen/commit/e378ed6199553f64d9e73ad27f9409ef501aa724) Thanks [@jplhomer](https://github.com/jplhomer)! - Reverts [#1272](https://github.com/Shopify/hydrogen/pull/1272) and properly escapes terminating script sequences
48
+
49
+ - [#1283](https://github.com/Shopify/hydrogen/pull/1283) [`eea82cb0`](https://github.com/Shopify/hydrogen/commit/eea82cb02064471d274e534c557caa5d3527bc93) Thanks [@jplhomer](https://github.com/jplhomer)! - Hydrogen has been updated to use the latest stable version of React.
50
+
51
+ To update an existing Hydrogen app:
52
+
53
+ ```bash
54
+ yarn add react@latest react-dom@latest
55
+ ```
56
+
3
57
  ## 0.20.0
4
58
 
5
59
  ### Minor Changes
@@ -11,4 +11,4 @@ export interface CartEstimatedCostProps {
11
11
  * cost associated with the `amountType` prop. If no `amountType` prop is specified, then it defaults to `totalAmount`.
12
12
  * If `children` is a function, then it will pass down the render props provided by the parent component.
13
13
  */
14
- export declare function CartEstimatedCost<TTag extends keyof JSX.IntrinsicElements>(props: Omit<React.ComponentProps<typeof Money>, 'data'> & CartEstimatedCostProps): JSX.Element | null;
14
+ export declare function CartEstimatedCost(props: Omit<React.ComponentProps<typeof Money>, 'data'> & CartEstimatedCostProps): JSX.Element | null;
@@ -8,5 +8,5 @@ interface CartLinePriceProps {
8
8
  * The `CartLinePrice` component renders a `Money` component for the cart line merchandise's price or
9
9
  * compare at price. It must be a descendent of a `CartLineProvider` component.
10
10
  */
11
- export declare function CartLinePrice<TTag extends keyof JSX.IntrinsicElements>(props: Omit<React.ComponentProps<typeof Money>, 'data'> & CartLinePriceProps): JSX.Element | null;
11
+ export declare function CartLinePrice(props: Omit<React.ComponentProps<typeof Money>, 'data'> & CartLinePriceProps): JSX.Element | null;
12
12
  export {};
@@ -1,19 +1,25 @@
1
- import type { MoneyV2 } from '../../storefront-api-types';
1
+ import React, { type ReactNode } from 'react';
2
+ import type { MoneyV2, UnitPriceMeasurement } from '../../storefront-api-types';
2
3
  import type { PartialDeep } from 'type-fest';
3
- interface MoneyProps<TTag> {
4
- /** An HTML tag to be rendered as the base element wrapper. The default is `div`. */
5
- as?: TTag;
4
+ interface CustomProps<ComponentGeneric extends React.ElementType> {
5
+ /** An HTML tag or React Component to be rendered as the base element wrapper. The default is `div`. */
6
+ as?: ComponentGeneric;
6
7
  /** An object with fields that correspond to the Storefront API's [MoneyV2 object](https://shopify.dev/api/storefront/reference/common-objects/moneyv2). */
7
8
  data: PartialDeep<MoneyV2>;
8
9
  /** Whether to remove the currency symbol from the output. */
9
10
  withoutCurrency?: boolean;
10
11
  /** Whether to remove trailing zeros (fractional money) from the output. */
11
12
  withoutTrailingZeros?: boolean;
13
+ /** A [UnitPriceMeasurement object](https://shopify.dev/api/storefront/latest/objects/unitpricemeasurement). */
14
+ measurement?: PartialDeep<UnitPriceMeasurement>;
15
+ /** Customizes the separator between the money output and the measurement output. Used with the `measurement` prop. Defaults to `'/'`. */
16
+ measurementSeparator?: ReactNode;
12
17
  }
18
+ declare type MoneyProps<ComponentGeneric extends React.ElementType> = CustomProps<ComponentGeneric> & Omit<React.ComponentPropsWithoutRef<ComponentGeneric>, keyof CustomProps<ComponentGeneric>>;
13
19
  /**
14
20
  * The `Money` component renders a string of the Storefront API's
15
21
  * [MoneyV2 object](https://shopify.dev/api/storefront/reference/common-objects/moneyv2) according to the
16
22
  * `defaultLocale` in [the `hydrogen.config.js` file](https://shopify.dev/custom-storefronts/hydrogen/framework/hydrogen-config).
17
23
  */
18
- export declare function Money<TTag extends keyof JSX.IntrinsicElements = 'div'>(props: JSX.IntrinsicElements[TTag] & MoneyProps<TTag>): JSX.Element;
24
+ export declare function Money<TTag extends React.ElementType>({ data, as, withoutCurrency, withoutTrailingZeros, measurement, measurementSeparator, ...passthroughProps }: MoneyProps<TTag>): JSX.Element;
19
25
  export {};
@@ -5,8 +5,10 @@ import { useMoney } from '../../hooks';
5
5
  * [MoneyV2 object](https://shopify.dev/api/storefront/reference/common-objects/moneyv2) according to the
6
6
  * `defaultLocale` in [the `hydrogen.config.js` file](https://shopify.dev/custom-storefronts/hydrogen/framework/hydrogen-config).
7
7
  */
8
- export function Money(props) {
9
- const { data, as, withoutCurrency, withoutTrailingZeros, ...passthroughProps } = props;
8
+ export function Money({ data, as, withoutCurrency, withoutTrailingZeros, measurement, measurementSeparator = '/', ...passthroughProps }) {
9
+ if (!isMoney(data)) {
10
+ throw new Error(`<Money/> needs a valid 'data' prop that has 'amount' and 'currencyCode'`);
11
+ }
10
12
  const moneyObject = useMoney(data);
11
13
  const Wrapper = as !== null && as !== void 0 ? as : 'div';
12
14
  let output = moneyObject.localizedString;
@@ -22,5 +24,16 @@ export function Money(props) {
22
24
  output = moneyObject.withoutTrailingZerosAndCurrency;
23
25
  }
24
26
  }
25
- return React.createElement(Wrapper, { ...passthroughProps }, output);
27
+ return (React.createElement(Wrapper, { ...passthroughProps },
28
+ output,
29
+ measurement && measurement.referenceUnit && (React.createElement(React.Fragment, null,
30
+ measurementSeparator,
31
+ measurement.referenceUnit))));
32
+ }
33
+ // required in order to narrow the money object down and make TS happy
34
+ function isMoney(maybeMoney) {
35
+ return (typeof maybeMoney.amount === 'string' &&
36
+ !!maybeMoney.amount &&
37
+ typeof maybeMoney.currencyCode === 'string' &&
38
+ !!maybeMoney.currencyCode);
26
39
  }
@@ -1,6 +1,5 @@
1
1
  import React from 'react';
2
2
  import { Money } from '../Money';
3
- import { UnitPrice } from '../UnitPrice';
4
3
  export interface ProductPriceProps {
5
4
  /** The type of price. Valid values: `regular` (default) or `compareAt`. */
6
5
  priceType?: 'regular' | 'compareAt';
@@ -13,4 +12,4 @@ export interface ProductPriceProps {
13
12
  * The `ProductPrice` component renders a `Money` component with the product
14
13
  * [`priceRange`](https://shopify.dev/api/storefront/reference/products/productpricerange)'s `maxVariantPrice` or `minVariantPrice`, for either the regular price or compare at price range. It must be a descendent of the `ProductProvider` component.
15
14
  */
16
- export declare function ProductPrice<TTag extends keyof JSX.IntrinsicElements>(props: (Omit<React.ComponentProps<typeof UnitPrice>, 'data' | 'measurement'> | Omit<React.ComponentProps<typeof Money>, 'data'>) & ProductPriceProps): JSX.Element | null;
15
+ export declare function ProductPrice<TTag extends keyof JSX.IntrinsicElements>(props: Omit<React.ComponentProps<typeof Money>, 'data' | 'measurement'> & ProductPriceProps): JSX.Element | null;
@@ -1,7 +1,6 @@
1
1
  import React from 'react';
2
2
  import { Money } from '../Money';
3
3
  import { useProduct } from '../ProductProvider';
4
- import { UnitPrice } from '../UnitPrice';
5
4
  /**
6
5
  * The `ProductPrice` component renders a `Money` component with the product
7
6
  * [`priceRange`](https://shopify.dev/api/storefront/reference/products/productpricerange)'s `maxVariantPrice` or `minVariantPrice`, for either the regular price or compare at price range. It must be a descendent of the `ProductProvider` component.
@@ -51,7 +50,7 @@ export function ProductPrice(props) {
51
50
  return null;
52
51
  }
53
52
  if (measurement) {
54
- return (React.createElement(UnitPrice, { ...passthroughProps, data: price, measurement: measurement }));
53
+ return (React.createElement(Money, { ...passthroughProps, data: price, measurement: measurement }));
55
54
  }
56
55
  return React.createElement(Money, { ...passthroughProps, data: price });
57
56
  }
@@ -21,10 +21,7 @@ export { CartEstimatedCost } from './CartEstimatedCost';
21
21
  export { CartProvider, useCart, useInstantCheckout } from './CartProvider';
22
22
  export type { State, Status, Cart, CartWithActions, CartAction, } from './CartProvider';
23
23
  export { ProductProvider, useProduct } from './ProductProvider';
24
- export { ProductDescription } from './ProductDescription';
25
- export { ProductTitle } from './ProductTitle';
26
24
  export { ProductPrice } from './ProductPrice';
27
- export { ProductMetafield } from './ProductMetafield';
28
25
  export { BuyNowButton } from './BuyNowButton';
29
26
  export { ShopPayButton } from './ShopPayButton';
30
27
  export { useCountry } from '../hooks/useCountry';
@@ -19,10 +19,7 @@ export { CartShopPayButton } from './CartShopPayButton';
19
19
  export { CartEstimatedCost } from './CartEstimatedCost';
20
20
  export { CartProvider, useCart, useInstantCheckout } from './CartProvider';
21
21
  export { ProductProvider, useProduct } from './ProductProvider';
22
- export { ProductDescription } from './ProductDescription';
23
- export { ProductTitle } from './ProductTitle';
24
22
  export { ProductPrice } from './ProductPrice';
25
- export { ProductMetafield } from './ProductMetafield';
26
23
  export { BuyNowButton } from './BuyNowButton';
27
24
  export { ShopPayButton } from './ShopPayButton';
28
25
  export { useCountry } from '../hooks/useCountry';
@@ -18,6 +18,7 @@ import { Analytics } from './foundation/Analytics/Analytics.server';
18
18
  import { ServerAnalyticsRoute } from './foundation/Analytics/ServerAnalyticsRoute.server';
19
19
  import { getSyncSessionApi } from './foundation/session/session';
20
20
  import { parseJSON } from './utilities/parse';
21
+ import { htmlEncode } from './utilities';
21
22
  const DOCTYPE = '<!DOCTYPE html>';
22
23
  const CONTENT_TYPE = 'Content-Type';
23
24
  const HTML_CONTENT_TYPE = 'text/html; charset=UTF-8';
@@ -63,7 +64,10 @@ export const renderHydrogen = (App, hydrogenConfig) => {
63
64
  : apiResponse;
64
65
  }
65
66
  }
66
- const isStreamable = !isBotUA(url, request.headers.get('user-agent')) &&
67
+ const isStreamable = (hydrogenConfig.enableStreaming
68
+ ? hydrogenConfig.enableStreaming(request)
69
+ : true) &&
70
+ !isBotUA(url, request.headers.get('user-agent')) &&
67
71
  (!!streamableResponse || (await isStreamingSupported()));
68
72
  let template = typeof indexTemplate === 'function'
69
73
  ? await indexTemplate(url.toString())
@@ -108,7 +112,7 @@ function getApiRoute(url, routes) {
108
112
  */
109
113
  async function render(url, { App, request, template, componentResponse, nonce, log }) {
110
114
  const state = { pathname: url.pathname, search: url.search };
111
- const { AppSSR } = buildAppSSR({
115
+ const { AppSSR, rscReadable } = buildAppSSR({
112
116
  App,
113
117
  log,
114
118
  state,
@@ -120,7 +124,10 @@ async function render(url, { App, request, template, componentResponse, nonce, l
120
124
  componentResponse.writeHead({ status: 500 });
121
125
  return template;
122
126
  }
123
- let html = await renderToBufferedString(AppSSR, { log, nonce }).catch(onErrorShell);
127
+ let [html, flight] = await Promise.all([
128
+ renderToBufferedString(AppSSR, { log, nonce }).catch(onErrorShell),
129
+ bufferReadableStream(rscReadable.getReader()).catch(() => null),
130
+ ]);
124
131
  const { headers, status, statusText } = getResponseOptions(componentResponse);
125
132
  /**
126
133
  * TODO: Also add `Vary` headers for `accept-language` and any other keys
@@ -138,6 +145,9 @@ async function render(url, { App, request, template, componentResponse, nonce, l
138
145
  }
139
146
  headers.set(CONTENT_TYPE, HTML_CONTENT_TYPE);
140
147
  html = applyHtmlHead(html, request.ctx.head, template);
148
+ if (flight) {
149
+ html = html.replace('</body>', () => flightContainer(flight) + '</body>');
150
+ }
141
151
  postRequestTasks('ssr', status, request, componentResponse);
142
152
  return new Response(html, {
143
153
  status,
@@ -164,7 +174,11 @@ async function stream(url, { App, request, response, componentResponse, template
164
174
  const rscToScriptTagReadable = new ReadableStream({
165
175
  start(controller) {
166
176
  log.trace('rsc start chunks');
167
- bufferReadableStream(rscReadable.getReader()).then(() => {
177
+ const encoder = new TextEncoder();
178
+ bufferReadableStream(rscReadable.getReader(), (chunk) => {
179
+ const metaTag = flightContainer(chunk);
180
+ controller.enqueue(encoder.encode(metaTag));
181
+ }).then(() => {
168
182
  log.trace('rsc finish chunks');
169
183
  return controller.close();
170
184
  });
@@ -354,34 +368,15 @@ async function hydrate(url, { App, log, request, response, isStreamable, compone
354
368
  request,
355
369
  response: componentResponse,
356
370
  });
357
- if (__WORKER__) {
358
- const rscReadable = rscRenderToReadableStream(AppRSC);
359
- if (isStreamable && (await isStreamingSupported())) {
360
- postRequestTasks('rsc', 200, request, componentResponse);
361
- return new Response(rscReadable);
362
- }
363
- // Note: CFW does not support reader.piteTo nor iterable syntax
364
- const bufferedBody = await bufferReadableStream(rscReadable.getReader());
365
- postRequestTasks('rsc', 200, request, componentResponse);
366
- return new Response(bufferedBody, {
367
- headers: {
368
- 'cache-control': componentResponse.cacheControlHeader,
369
- },
370
- });
371
- }
372
- else if (response) {
373
- const rscWriter = await import(
374
- // @ts-ignore
375
- '@shopify/hydrogen/vendor/react-server-dom-vite/writer.node.server');
376
- const streamer = rscWriter.renderToPipeableStream(AppRSC);
377
- response.writeHead(200, 'ok', {
371
+ const rscReadable = rscRenderToReadableStream(AppRSC);
372
+ // Note: CFW does not support reader.piteTo nor iterable syntax
373
+ const bufferedBody = await bufferReadableStream(rscReadable.getReader());
374
+ postRequestTasks('rsc', 200, request, componentResponse);
375
+ return new Response(bufferedBody, {
376
+ headers: {
378
377
  'cache-control': componentResponse.cacheControlHeader,
379
- });
380
- const stream = streamer.pipe(response);
381
- stream.on('finish', function () {
382
- postRequestTasks('rsc', response.statusCode, request, componentResponse);
383
- });
384
- }
378
+ },
379
+ });
385
380
  }
386
381
  function buildAppRSC({ App, log, state, request, response }) {
387
382
  const hydrogenServerProps = { request, response, log };
@@ -512,6 +507,9 @@ async function createNodeWriter() {
512
507
  const { PassThrough } = await import(streamImport);
513
508
  return new PassThrough();
514
509
  }
510
+ function flightContainer(chunk) {
511
+ return `<meta data-flight="${htmlEncode(chunk)}" />`;
512
+ }
515
513
  function postRequestTasks(type, status, request, componentResponse) {
516
514
  logServerResponse(type, request, status);
517
515
  logCacheControlHeaders(type, request, componentResponse);
@@ -9,12 +9,27 @@ function requestCacheRSC() {
9
9
  return new Map();
10
10
  }
11
11
  requestCacheRSC.key = Symbol.for('HYDROGEN_REQUEST');
12
+ // Note: use this only during RSC/Flight rendering. The React dispatcher
13
+ // for SSR/Fizz rendering does not implement getCacheForType.
14
+ function getCacheForType(resource) {
15
+ var _a;
16
+ const dispatcher =
17
+ // @ts-ignore
18
+ React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
19
+ .ReactCurrentDispatcher.current;
20
+ // @ts-ignore
21
+ if (__DEV__ && typeof jest !== 'undefined' && !dispatcher.getCacheForType) {
22
+ // Jest does not have access to the RSC runtime, mock it here:
23
+ // @ts-ignore
24
+ return ((_a = globalThis.__jestRscCache) !== null && _a !== void 0 ? _a : (globalThis.__jestRscCache = resource()));
25
+ }
26
+ return dispatcher.getCacheForType(resource);
27
+ }
12
28
  export function ServerRequestProvider({ isRSC, request, children, }) {
13
29
  if (isRSC) {
14
30
  // Save the request object in a React cache that is
15
31
  // scoped to this current rendering.
16
- // @ts-ignore
17
- const requestCache = React.unstable_getCacheForType(requestCacheRSC);
32
+ const requestCache = getCacheForType(requestCacheRSC);
18
33
  requestCache.set(requestCacheRSC.key, request);
19
34
  return children;
20
35
  }
@@ -27,7 +42,7 @@ export function useServerRequest() {
27
42
  try {
28
43
  // This cache only works during RSC rendering:
29
44
  // @ts-ignore
30
- const cache = React.unstable_getCacheForType(requestCacheRSC);
45
+ const cache = getCacheForType(requestCacheRSC);
31
46
  request = cache ? cache.get(requestCacheRSC.key) : null;
32
47
  }
33
48
  catch (_a) {
@@ -1,8 +1,8 @@
1
- import { getLoggerWithContext, collectQueryCacheControlHeaders, collectQueryTimings, logCacheApiStatus, } from '../../utilities/log';
2
- import { deleteItemFromCache, generateSubRequestCacheControlHeader, getItemFromCache, isStale, setItemInCache, } from '../../framework/cache';
3
- import { hashKey } from '../../utilities/hash';
1
+ import { getLoggerWithContext, collectQueryCacheControlHeaders, collectQueryTimings, } from '../../utilities/log';
2
+ import { deleteItemFromCache, generateSubRequestCacheControlHeader, getItemFromCache, isStale, setItemInCache, } from '../../framework/cache-sub-request';
4
3
  import { runDelayedFunction } from '../../framework/runtime';
5
4
  import { useRequestCacheData, useServerRequest } from '../ServerRequestProvider';
5
+ import { CacheSeconds } from '../../framework/CachingStrategy';
6
6
  /**
7
7
  * The `useQuery` hook executes an asynchronous operation like `fetch` in a way that
8
8
  * supports [Suspense](https://reactjs.org/docs/concurrent-mode-suspense.html). You can use this
@@ -48,7 +48,6 @@ function cachedQueryFnBuilder(key, queryFn, queryOptions) {
48
48
  // to prevent losing the current React cycle.
49
49
  const request = useServerRequest();
50
50
  const log = getLoggerWithContext(request);
51
- const hashedKey = hashKey(key);
52
51
  const cacheResponse = await getItemFromCache(key);
53
52
  async function generateNewOutput() {
54
53
  return await queryFn();
@@ -59,15 +58,15 @@ function cachedQueryFnBuilder(key, queryFn, queryOptions) {
59
58
  /**
60
59
  * Important: Do this async
61
60
  */
62
- if (isStale(response, resolvedQueryOptions === null || resolvedQueryOptions === void 0 ? void 0 : resolvedQueryOptions.cache)) {
63
- logCacheApiStatus('STALE', hashedKey);
64
- const lockKey = `lock-${key}`;
61
+ if (isStale(key, response)) {
62
+ const lockKey = ['lock', ...(typeof key === 'string' ? [key] : key)];
65
63
  runDelayedFunction(async () => {
66
- logCacheApiStatus('UPDATING', hashedKey);
67
64
  const lockExists = await getItemFromCache(lockKey);
68
65
  if (lockExists)
69
66
  return;
70
- await setItemInCache(lockKey, true);
67
+ await setItemInCache(lockKey, true, CacheSeconds({
68
+ maxAge: 10,
69
+ }));
71
70
  try {
72
71
  const output = await generateNewOutput();
73
72
  if (shouldCacheResponse(output)) {
@@ -2,7 +2,7 @@ import type { CachingStrategy } from '../../types';
2
2
  import React from 'react';
3
3
  export declare class ServerComponentResponse extends Response {
4
4
  private wait;
5
- private cacheOptions?;
5
+ private cacheOptions;
6
6
  customStatus?: {
7
7
  code?: number;
8
8
  text?: string;
@@ -6,6 +6,7 @@ export class ServerComponentResponse extends Response {
6
6
  constructor() {
7
7
  super(...arguments);
8
8
  this.wait = false;
9
+ this.cacheOptions = CacheSeconds();
9
10
  /**
10
11
  * Allow custom body to be a string or a Promise.
11
12
  */
@@ -25,7 +26,7 @@ export class ServerComponentResponse extends Response {
25
26
  this.cacheOptions = options;
26
27
  }
27
28
  get cacheControlHeader() {
28
- return generateCacheControlHeader(this.cacheOptions || CacheSeconds());
29
+ return generateCacheControlHeader(this.cacheOptions);
29
30
  }
30
31
  writeHead({ status, statusText, headers, } = {}) {
31
32
  if (status || statusText) {
@@ -1,15 +1,65 @@
1
1
  // TODO should we move this file to src/foundation
2
2
  // so it is considered ESM instead of CJS?
3
- // @ts-ignore
4
- import { unstable_getCacheForType, unstable_useCacheRefresh } from 'react';
5
3
  import { createFromFetch, createFromReadableStream,
6
4
  // @ts-ignore
7
5
  } from '@shopify/hydrogen/vendor/react-server-dom-vite';
8
6
  import { RSC_PATHNAME } from '../../constants';
7
+ import { htmlDecode } from '../../utilities';
9
8
  let rscReader;
10
- function createResponseCache() {
11
- return new Map();
9
+ // Hydrate an SSR response from <meta> tags placed in the DOM.
10
+ const flightChunks = [];
11
+ const FLIGHT_ATTRIBUTE = 'data-flight';
12
+ function addElementToFlightChunks(el) {
13
+ const chunk = el.getAttribute(FLIGHT_ATTRIBUTE);
14
+ if (chunk) {
15
+ flightChunks.push(htmlDecode(chunk));
16
+ }
17
+ }
18
+ // Get initial payload
19
+ document
20
+ .querySelectorAll('[' + FLIGHT_ATTRIBUTE + ']')
21
+ .forEach(addElementToFlightChunks);
22
+ // Create a mutation observer on the document to detect when new
23
+ // <meta data-flight> tags are added, and add them to the array.
24
+ const observer = new MutationObserver((mutations) => {
25
+ mutations.forEach((mutation) => {
26
+ mutation.addedNodes.forEach((node) => {
27
+ if (node instanceof HTMLElement &&
28
+ node.tagName === 'META' &&
29
+ node.hasAttribute(FLIGHT_ATTRIBUTE)) {
30
+ addElementToFlightChunks(node);
31
+ }
32
+ });
33
+ });
34
+ });
35
+ observer.observe(document.documentElement, {
36
+ childList: true,
37
+ subtree: true,
38
+ });
39
+ if (flightChunks.length > 0) {
40
+ const contentLoaded = new Promise((resolve) => document.addEventListener('DOMContentLoaded', resolve));
41
+ try {
42
+ rscReader = new ReadableStream({
43
+ start(controller) {
44
+ const encoder = new TextEncoder();
45
+ const write = (chunk) => {
46
+ controller.enqueue(encoder.encode(chunk));
47
+ return 0;
48
+ };
49
+ flightChunks.forEach(write);
50
+ flightChunks.push = write;
51
+ contentLoaded.then(() => {
52
+ controller.close();
53
+ observer.disconnect();
54
+ });
55
+ },
56
+ });
57
+ }
58
+ catch (_) {
59
+ // Old browser, will try a new hydration request later
60
+ }
12
61
  }
62
+ const cache = new Map();
13
63
  /**
14
64
  * Much of this is borrowed from React's demo implementation:
15
65
  * @see https://github.com/reactjs/server-components-demo/blob/main/src/Cache.client.js
@@ -18,7 +68,6 @@ function createResponseCache() {
18
68
  */
19
69
  export function useServerResponse(state) {
20
70
  const key = JSON.stringify(state);
21
- const cache = unstable_getCacheForType(createResponseCache);
22
71
  let response = cache.get(key);
23
72
  if (response) {
24
73
  return response;
@@ -47,6 +96,5 @@ export function useServerResponse(state) {
47
96
  return response;
48
97
  }
49
98
  export function useRefresh() {
50
- const refreshCache = unstable_useCacheRefresh();
51
- refreshCache();
99
+ cache.clear();
52
100
  }
@@ -1,4 +1,3 @@
1
- import { logCacheApiStatus } from '../../utilities/log';
2
1
  /**
3
2
  * This is an in-memory implementation of `Cache` that *barely*
4
3
  * works and is only meant to be used during development.
@@ -8,7 +7,6 @@ export class InMemoryCache {
8
7
  this.store = new Map();
9
8
  }
10
9
  put(request, response) {
11
- logCacheApiStatus('PUT-dev', request.url);
12
10
  this.store.set(request.url, {
13
11
  value: response,
14
12
  date: new Date(),
@@ -18,7 +16,6 @@ export class InMemoryCache {
18
16
  var _a, _b;
19
17
  const match = this.store.get(request.url);
20
18
  if (!match) {
21
- logCacheApiStatus('MISS-dev', request.url);
22
19
  return;
23
20
  }
24
21
  const { value, date } = match;
@@ -28,7 +25,6 @@ export class InMemoryCache {
28
25
  const age = (new Date().valueOf() - date.valueOf()) / 1000;
29
26
  const isMiss = age > maxAge + swr;
30
27
  if (isMiss) {
31
- logCacheApiStatus('MISS-dev', request.url);
32
28
  this.store.delete(request.url);
33
29
  return;
34
30
  }
@@ -36,7 +32,6 @@ export class InMemoryCache {
36
32
  const headers = new Headers(value.headers);
37
33
  headers.set('cache', isStale ? 'STALE' : 'HIT');
38
34
  headers.set('date', date.toUTCString());
39
- logCacheApiStatus(`${headers.get('cache')}-dev`, request.url);
40
35
  const response = new Response(value.body, {
41
36
  headers,
42
37
  });
@@ -44,7 +39,6 @@ export class InMemoryCache {
44
39
  }
45
40
  delete(request) {
46
41
  this.store.delete(request.url);
47
- logCacheApiStatus('DELETE-dev', request.url);
48
42
  }
49
43
  keys(request) {
50
44
  const cacheKeys = [];
@@ -0,0 +1,17 @@
1
+ import type { QueryKey, CachingStrategy } from '../types';
2
+ export declare function generateSubRequestCacheControlHeader(userCacheOptions?: CachingStrategy): string;
3
+ /**
4
+ * Get an item from the cache. If a match is found, returns a tuple
5
+ * containing the `JSON.parse` version of the response as well
6
+ * as the response itself so it can be checked for staleness.
7
+ */
8
+ export declare function getItemFromCache(key: QueryKey): Promise<undefined | [any, Response]>;
9
+ /**
10
+ * Put an item into the cache.
11
+ */
12
+ export declare function setItemInCache(key: QueryKey, value: any, userCacheOptions?: CachingStrategy): Promise<void>;
13
+ export declare function deleteItemFromCache(key: QueryKey): Promise<void>;
14
+ /**
15
+ * Manually check the response to see if it's stale.
16
+ */
17
+ export declare function isStale(key: QueryKey, response: Response): boolean;
@@ -0,0 +1,64 @@
1
+ import { getCache } from './runtime';
2
+ import { hashKey } from '../utilities/hash';
3
+ import * as CacheApi from './cache';
4
+ import { CacheSeconds } from './CachingStrategy';
5
+ /**
6
+ * Wrapper Cache functions for sub queries
7
+ */
8
+ /**
9
+ * Cache API is weird. We just need a full URL, so we make one up.
10
+ */
11
+ function getKeyUrl(key) {
12
+ return `https://shopify.dev/?${key}`;
13
+ }
14
+ function getCacheOption(userCacheOptions) {
15
+ return userCacheOptions || CacheSeconds();
16
+ }
17
+ export function generateSubRequestCacheControlHeader(userCacheOptions) {
18
+ return CacheApi.generateDefaultCacheControlHeader(getCacheOption(userCacheOptions));
19
+ }
20
+ /**
21
+ * Get an item from the cache. If a match is found, returns a tuple
22
+ * containing the `JSON.parse` version of the response as well
23
+ * as the response itself so it can be checked for staleness.
24
+ */
25
+ export async function getItemFromCache(key) {
26
+ const cache = getCache();
27
+ if (!cache) {
28
+ return;
29
+ }
30
+ const url = getKeyUrl(hashKey(key));
31
+ const request = new Request(url);
32
+ const response = await CacheApi.getItemFromCache(request);
33
+ if (!response) {
34
+ return;
35
+ }
36
+ return [await response.json(), response];
37
+ }
38
+ /**
39
+ * Put an item into the cache.
40
+ */
41
+ export async function setItemInCache(key, value, userCacheOptions) {
42
+ const cache = getCache();
43
+ if (!cache) {
44
+ return;
45
+ }
46
+ const url = getKeyUrl(hashKey(key));
47
+ const request = new Request(url);
48
+ const response = new Response(JSON.stringify(value));
49
+ await CacheApi.setItemInCache(request, response, getCacheOption(userCacheOptions));
50
+ }
51
+ export async function deleteItemFromCache(key) {
52
+ const cache = getCache();
53
+ if (!cache)
54
+ return;
55
+ const url = getKeyUrl(hashKey(key));
56
+ const request = new Request(url);
57
+ await CacheApi.deleteItemFromCache(request);
58
+ }
59
+ /**
60
+ * Manually check the response to see if it's stale.
61
+ */
62
+ export function isStale(key, response) {
63
+ return CacheApi.isStale(new Request(getKeyUrl(hashKey(key))), response);
64
+ }