@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.
- package/README.md +1 -1
- package/dist/esnext/components/CartProvider/CartProvider.client.js +6 -0
- package/dist/esnext/components/CartProvider/cart-queries.d.ts +1 -1
- package/dist/esnext/components/CartProvider/cart-queries.js +1 -0
- package/dist/esnext/components/ExternalVideo/ExternalVideo.js +2 -2
- package/dist/esnext/components/Image/Image.d.ts +1 -1
- package/dist/esnext/components/Image/Image.js +4 -4
- package/dist/esnext/components/Image/index.d.ts +1 -1
- package/dist/esnext/components/index.d.ts +1 -1
- package/dist/esnext/components/index.js +1 -1
- package/dist/esnext/constants.d.ts +1 -0
- package/dist/esnext/constants.js +1 -0
- package/dist/esnext/entry-client.js +1 -1
- package/dist/esnext/entry-server.js +30 -28
- package/dist/esnext/experimental.d.ts +1 -0
- package/dist/esnext/experimental.js +1 -0
- package/dist/esnext/foundation/Cache/cache.js +0 -1
- package/dist/esnext/foundation/HydrogenRequest/HydrogenRequest.server.d.ts +1 -0
- package/dist/esnext/foundation/HydrogenRequest/HydrogenRequest.server.js +1 -0
- package/dist/esnext/foundation/HydrogenResponse/HydrogenResponse.server.d.ts +4 -2
- package/dist/esnext/foundation/HydrogenResponse/HydrogenResponse.server.js +17 -16
- package/dist/esnext/foundation/ServerPropsProvider/ServerPropsProvider.js +1 -3
- package/dist/esnext/foundation/fetchSync/ResponseSync.d.ts +2 -1
- package/dist/esnext/foundation/fetchSync/ResponseSync.js +14 -2
- package/dist/esnext/foundation/session/session-types.d.ts +2 -0
- package/dist/esnext/foundation/session/session.d.ts +3 -0
- package/dist/esnext/foundation/session/session.js +16 -0
- package/dist/esnext/foundation/useQuery/hooks.d.ts +3 -0
- package/dist/esnext/foundation/useQuery/hooks.js +1 -1
- package/dist/esnext/foundation/useSession/useSession.d.ts +1 -0
- package/dist/esnext/foundation/useSession/useSession.js +13 -0
- package/dist/esnext/framework/plugin.js +6 -5
- package/dist/esnext/framework/plugins/vite-plugin-assets-version.d.ts +2 -0
- package/dist/esnext/framework/plugins/vite-plugin-assets-version.js +12 -0
- package/dist/esnext/framework/plugins/vite-plugin-client-imports.js +6 -3
- package/dist/esnext/framework/plugins/vite-plugin-css-modules-rsc.js +3 -0
- package/dist/esnext/framework/plugins/vite-plugin-css-rsc.js +8 -5
- package/dist/esnext/framework/plugins/vite-plugin-hydration-auto-import.js +7 -1
- 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
- package/dist/esnext/framework/plugins/{vite-plugin-hydrogen-client-middleware.js → vite-plugin-hydrogen-client-components-cache.js} +2 -2
- package/dist/esnext/framework/plugins/vite-plugin-hydrogen-config.d.ts +2 -1
- package/dist/esnext/framework/plugins/vite-plugin-hydrogen-config.js +104 -90
- package/dist/esnext/framework/plugins/vite-plugin-hydrogen-middleware.js +6 -6
- package/dist/esnext/framework/plugins/vite-plugin-hydrogen-suppress-warnings.js +5 -0
- package/dist/esnext/framework/plugins/vite-plugin-hydrogen-virtual-files.js +45 -7
- package/dist/esnext/framework/plugins/vite-plugin-platform-entry.js +14 -1
- package/dist/esnext/framework/plugins/vite-plugin-ssr-interop.js +5 -1
- package/dist/esnext/framework/types.d.ts +1 -0
- package/dist/esnext/framework/viteception.js +7 -1
- package/dist/esnext/storefront-api-types.d.ts +38 -31
- package/dist/esnext/storefront-api-types.js +4 -2
- package/dist/esnext/types.d.ts +2 -1
- package/dist/esnext/utilities/apiRoutes.d.ts +2 -0
- package/dist/esnext/utilities/apiRoutes.js +12 -1
- package/dist/esnext/utilities/fetch.js +4 -1
- package/dist/esnext/utilities/log/log.js +20 -5
- package/dist/esnext/utilities/log/utils.js +1 -1
- package/dist/esnext/utilities/template.d.ts +7 -6
- package/dist/esnext/utilities/template.js +39 -1
- package/dist/esnext/utilities/vite.d.ts +1 -0
- package/dist/esnext/utilities/vite.js +4 -0
- package/dist/esnext/version.d.ts +1 -1
- package/dist/esnext/version.js +1 -1
- package/dist/node/foundation/session/session-types.d.ts +2 -0
- package/dist/node/framework/plugin.js +6 -5
- package/dist/node/framework/plugins/vite-plugin-assets-version.d.ts +2 -0
- package/dist/node/framework/plugins/vite-plugin-assets-version.js +15 -0
- package/dist/node/framework/plugins/vite-plugin-client-imports.js +6 -3
- package/dist/node/framework/plugins/vite-plugin-css-modules-rsc.js +3 -0
- package/dist/node/framework/plugins/vite-plugin-css-rsc.js +8 -5
- package/dist/node/framework/plugins/vite-plugin-hydration-auto-import.js +7 -1
- 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
- package/dist/node/framework/plugins/{vite-plugin-hydrogen-client-middleware.js → vite-plugin-hydrogen-client-components-cache.js} +2 -2
- package/dist/node/framework/plugins/vite-plugin-hydrogen-config.d.ts +2 -1
- package/dist/node/framework/plugins/vite-plugin-hydrogen-config.js +107 -90
- package/dist/node/framework/plugins/vite-plugin-hydrogen-middleware.js +6 -6
- package/dist/node/framework/plugins/vite-plugin-hydrogen-suppress-warnings.js +5 -0
- package/dist/node/framework/plugins/vite-plugin-hydrogen-virtual-files.js +45 -7
- package/dist/node/framework/plugins/vite-plugin-platform-entry.js +14 -1
- package/dist/node/framework/plugins/vite-plugin-ssr-interop.js +5 -1
- package/dist/node/framework/types.d.ts +1 -0
- package/dist/node/framework/viteception.js +7 -1
- package/dist/node/utilities/vite.d.ts +1 -0
- package/dist/node/utilities/vite.js +30 -0
- package/package.json +7 -6
- package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-plugin.js +36 -25
- package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-writer.browser.development.server.js +105 -29
- package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-writer.browser.production.min.server.js +30 -29
- package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-writer.node.development.server.js +92 -29
- package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-writer.node.production.min.server.js +31 -29
- package/vendor/react-server-dom-vite/esm/react-server-dom-vite-plugin.js +29 -18
- package/vendor/react-server-dom-vite/esm/react-server-dom-vite-writer.browser.server.js +105 -29
- package/vendor/react-server-dom-vite/esm/react-server-dom-vite-writer.node.server.js +92 -29
- package/vendor/react-server-dom-vite/package.json +2 -2
- package/dist/esnext/framework/plugins/vite-plugin-hydrogen-client-middleware.d.ts +0 -9
- package/dist/esnext/framework/plugins/vite-plugin-purge-query-cache.js +0 -11
- package/dist/node/framework/plugins/vite-plugin-hydrogen-client-middleware.d.ts +0 -9
- package/dist/node/framework/plugins/vite-plugin-purge-query-cache.js +0 -16
package/README.md
CHANGED
|
@@ -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";
|
|
@@ -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";
|
package/dist/esnext/constants.js
CHANGED
|
@@ -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 {
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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
|
}
|
|
@@ -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;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
22
|
+
__HYDROGEN_CACHE_ID__,
|
|
23
23
|
...(typeof key === 'string' ? [key] : key),
|
|
24
24
|
];
|
|
25
25
|
const fetcher = cachedQueryFnBuilder(withCacheIdKey, queryFn, queryOptions);
|
|
@@ -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
|
+
};
|