@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.
- package/CHANGELOG.md +54 -0
- package/dist/esnext/components/CartEstimatedCost/CartEstimatedCost.client.d.ts +1 -1
- package/dist/esnext/components/CartLinePrice/CartLinePrice.client.d.ts +1 -1
- package/dist/esnext/components/Money/Money.client.d.ts +11 -5
- package/dist/esnext/components/Money/Money.client.js +16 -3
- package/dist/esnext/components/ProductPrice/ProductPrice.client.d.ts +1 -2
- package/dist/esnext/components/ProductPrice/ProductPrice.client.js +1 -2
- package/dist/esnext/components/index.d.ts +0 -3
- package/dist/esnext/components/index.js +0 -3
- package/dist/esnext/entry-server.js +29 -31
- package/dist/esnext/foundation/ServerRequestProvider/ServerRequestProvider.js +18 -3
- package/dist/esnext/foundation/useQuery/hooks.js +8 -9
- package/dist/esnext/framework/Hydration/ServerComponentResponse.server.d.ts +1 -1
- package/dist/esnext/framework/Hydration/ServerComponentResponse.server.js +2 -1
- package/dist/esnext/framework/Hydration/rsc.js +55 -7
- package/dist/esnext/framework/cache/in-memory.js +0 -6
- package/dist/esnext/framework/cache-sub-request.d.ts +17 -0
- package/dist/esnext/framework/cache-sub-request.js +64 -0
- package/dist/esnext/framework/cache.d.ts +6 -6
- package/dist/esnext/framework/cache.js +36 -33
- package/dist/esnext/framework/plugin.js +2 -0
- package/dist/esnext/framework/plugins/vite-plugin-client-imports.d.ts +2 -0
- package/dist/esnext/framework/plugins/vite-plugin-client-imports.js +25 -0
- package/dist/esnext/framework/plugins/vite-plugin-css-modules-rsc.js +8 -3
- package/dist/esnext/framework/plugins/vite-plugin-hydrogen-config.js +1 -0
- package/dist/esnext/index.d.ts +4 -0
- package/dist/esnext/index.js +4 -0
- package/dist/esnext/types.d.ts +1 -0
- package/dist/esnext/utilities/html-encoding.d.ts +2 -0
- package/dist/esnext/utilities/html-encoding.js +16 -0
- package/dist/esnext/utilities/index.d.ts +1 -0
- package/dist/esnext/utilities/index.js +1 -0
- package/dist/esnext/utilities/log/log-cache-api-status.js +5 -1
- package/dist/esnext/version.d.ts +1 -1
- package/dist/esnext/version.js +1 -1
- package/dist/node/entry-server.js +29 -31
- package/dist/node/foundation/ServerRequestProvider/ServerRequestProvider.js +18 -3
- package/dist/node/framework/Hydration/ServerComponentResponse.server.d.ts +1 -1
- package/dist/node/framework/Hydration/ServerComponentResponse.server.js +2 -1
- package/dist/node/framework/Hydration/rsc.js +55 -7
- package/dist/node/framework/cache/in-memory.js +0 -6
- package/dist/node/framework/cache-sub-request.d.ts +17 -0
- package/dist/node/framework/cache-sub-request.js +95 -0
- package/dist/node/framework/cache.d.ts +6 -6
- package/dist/node/framework/cache.js +38 -35
- package/dist/node/framework/plugin.js +2 -0
- package/dist/node/framework/plugins/vite-plugin-client-imports.d.ts +2 -0
- package/dist/node/framework/plugins/vite-plugin-client-imports.js +28 -0
- package/dist/node/framework/plugins/vite-plugin-css-modules-rsc.js +8 -3
- package/dist/node/framework/plugins/vite-plugin-hydrogen-config.js +1 -0
- package/dist/node/types.d.ts +1 -0
- package/dist/node/utilities/html-encoding.d.ts +2 -0
- package/dist/node/utilities/html-encoding.js +21 -0
- package/dist/node/utilities/index.d.ts +1 -0
- package/dist/node/utilities/index.js +4 -1
- package/dist/node/utilities/log/log-cache-api-status.js +5 -1
- package/dist/node/version.d.ts +1 -1
- package/dist/node/version.js +1 -1
- package/package.json +3 -3
- package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-plugin.js +29 -4
- package/vendor/react-server-dom-vite/esm/react-server-dom-vite-plugin.js +29 -4
- package/vendor/react-server-dom-vite/package.json +2 -1
- package/dist/esnext/components/ProductDescription/ProductDescription.client.d.ts +0 -13
- package/dist/esnext/components/ProductDescription/ProductDescription.client.js +0 -16
- package/dist/esnext/components/ProductDescription/index.d.ts +0 -1
- package/dist/esnext/components/ProductDescription/index.js +0 -1
- package/dist/esnext/components/ProductMetafield/ProductMetafield.client.d.ts +0 -21
- package/dist/esnext/components/ProductMetafield/ProductMetafield.client.js +0 -42
- package/dist/esnext/components/ProductMetafield/index.d.ts +0 -2
- package/dist/esnext/components/ProductMetafield/index.js +0 -1
- package/dist/esnext/components/ProductTitle/ProductTitle.client.d.ts +0 -13
- package/dist/esnext/components/ProductTitle/ProductTitle.client.js +0 -16
- package/dist/esnext/components/ProductTitle/index.d.ts +0 -1
- package/dist/esnext/components/ProductTitle/index.js +0 -1
- package/dist/esnext/components/UnitPrice/UnitPrice.client.d.ts +0 -15
- package/dist/esnext/components/UnitPrice/UnitPrice.client.js +0 -22
- package/dist/esnext/components/UnitPrice/index.d.ts +0 -1
- 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
|
|
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
|
|
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
|
|
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
|
|
4
|
-
/** An HTML tag to be rendered as the base element wrapper. The default is `div`. */
|
|
5
|
-
as?:
|
|
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
|
|
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(
|
|
9
|
-
|
|
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 },
|
|
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:
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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,
|
|
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(
|
|
63
|
-
|
|
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)) {
|
|
@@ -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
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
+
}
|