@shopify/hydrogen 0.16.1 → 0.17.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 (130) hide show
  1. package/CHANGELOG.md +85 -1
  2. package/dist/esnext/FileSessionStorage.d.ts +1 -0
  3. package/dist/esnext/FileSessionStorage.js +1 -0
  4. package/dist/esnext/client.d.ts +2 -2
  5. package/dist/esnext/client.js +2 -2
  6. package/dist/esnext/components/CartProvider/CartProvider.client.d.ts +1 -1
  7. package/dist/esnext/components/CartProvider/CartProvider.client.js +3 -3
  8. package/dist/esnext/components/ExternalVideo/ExternalVideo.d.ts +2 -2
  9. package/dist/esnext/components/ExternalVideo/ExternalVideo.js +1 -1
  10. package/dist/esnext/components/Image/Image.d.ts +2 -2
  11. package/dist/esnext/components/Image/Image.js +1 -1
  12. package/dist/esnext/components/Link/Link.client.d.ts +1 -1
  13. package/dist/esnext/components/Link/Link.client.js +3 -3
  14. package/dist/esnext/components/LocalizationProvider/LocalizationClientProvider.client.js +4 -4
  15. package/dist/esnext/components/LocalizationProvider/LocalizationProvider.server.d.ts +2 -2
  16. package/dist/esnext/components/LocalizationProvider/LocalizationProvider.server.js +2 -2
  17. package/dist/esnext/components/MediaFile/MediaFile.d.ts +2 -2
  18. package/dist/esnext/components/MediaFile/MediaFile.js +1 -1
  19. package/dist/esnext/components/Metafield/Metafield.client.d.ts +8 -4
  20. package/dist/esnext/components/Metafield/Metafield.client.js +6 -2
  21. package/dist/esnext/components/ModelViewer/ModelViewer.client.d.ts +3 -3
  22. package/dist/esnext/components/ModelViewer/ModelViewer.client.js +1 -1
  23. package/dist/esnext/components/Money/Money.client.d.ts +2 -2
  24. package/dist/esnext/components/Money/Money.client.js +1 -1
  25. package/dist/esnext/components/ProductDescription/ProductDescription.client.d.ts +1 -1
  26. package/dist/esnext/components/ProductDescription/ProductDescription.client.js +1 -1
  27. package/dist/esnext/components/ProductMetafield/ProductMetafield.client.d.ts +3 -3
  28. package/dist/esnext/components/ProductMetafield/ProductMetafield.client.js +1 -1
  29. package/dist/esnext/components/ProductPrice/ProductPrice.client.d.ts +1 -1
  30. package/dist/esnext/components/ProductPrice/ProductPrice.client.js +1 -1
  31. package/dist/esnext/components/ProductProvider/ProductProvider.client.d.ts +1 -1
  32. package/dist/esnext/components/ProductTitle/ProductTitle.client.d.ts +1 -1
  33. package/dist/esnext/components/ProductTitle/ProductTitle.client.js +1 -1
  34. package/dist/esnext/components/UnitPrice/UnitPrice.client.d.ts +4 -4
  35. package/dist/esnext/components/UnitPrice/UnitPrice.client.js +2 -2
  36. package/dist/esnext/components/Video/Video.d.ts +2 -2
  37. package/dist/esnext/components/Video/Video.js +1 -1
  38. package/dist/esnext/entry-client.js +4 -4
  39. package/dist/esnext/entry-server.d.ts +1 -1
  40. package/dist/esnext/entry-server.js +12 -5
  41. package/dist/esnext/foundation/Analytics/Analytics.server.js +4 -2
  42. package/dist/esnext/foundation/AnalyticsErrorBoundary.client.d.ts +4 -0
  43. package/dist/esnext/foundation/AnalyticsErrorBoundary.client.js +8 -0
  44. package/dist/esnext/foundation/Cookie/Cookie.d.ts +48 -0
  45. package/dist/esnext/foundation/Cookie/Cookie.js +66 -0
  46. package/dist/esnext/foundation/CookieSessionStorage/CookieSessionStorage.d.ts +5 -0
  47. package/dist/esnext/foundation/CookieSessionStorage/CookieSessionStorage.js +31 -0
  48. package/dist/esnext/foundation/FileSessionStorage/FileSessionStorage.d.ts +6 -0
  49. package/dist/esnext/foundation/FileSessionStorage/FileSessionStorage.js +148 -0
  50. package/dist/esnext/foundation/MemorySessionStorage/MemorySessionStorage.d.ts +5 -0
  51. package/dist/esnext/foundation/MemorySessionStorage/MemorySessionStorage.js +53 -0
  52. package/dist/esnext/foundation/Router/BrowserRouter.client.js +8 -8
  53. package/dist/esnext/foundation/ServerPropsProvider/ServerPropsProvider.d.ts +40 -0
  54. package/dist/esnext/foundation/ServerPropsProvider/ServerPropsProvider.js +76 -0
  55. package/dist/esnext/foundation/ServerPropsProvider/index.d.ts +2 -0
  56. package/dist/esnext/foundation/ServerPropsProvider/index.js +1 -0
  57. package/dist/esnext/foundation/index.d.ts +1 -1
  58. package/dist/esnext/foundation/index.js +1 -1
  59. package/dist/esnext/foundation/session/session.d.ts +27 -0
  60. package/dist/esnext/foundation/session/session.js +37 -0
  61. package/dist/esnext/foundation/useQuery/hooks.d.ts +3 -3
  62. package/dist/esnext/foundation/useQuery/hooks.js +1 -1
  63. package/dist/esnext/foundation/useServerProps/index.d.ts +1 -0
  64. package/dist/esnext/foundation/useServerProps/index.js +1 -0
  65. package/dist/esnext/foundation/useServerProps/use-server-props.d.ts +21 -0
  66. package/dist/esnext/foundation/useServerProps/use-server-props.js +35 -0
  67. package/dist/esnext/foundation/useSession/useSession.d.ts +2 -0
  68. package/dist/esnext/foundation/useSession/useSession.js +8 -0
  69. package/dist/esnext/framework/Hydration/ServerComponentRequest.server.d.ts +2 -0
  70. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-config.js +25 -12
  71. package/dist/esnext/hooks/useMoney/hooks.d.ts +1 -1
  72. package/dist/esnext/hooks/useMoney/hooks.js +1 -1
  73. package/dist/esnext/hooks/useParsedMetafields/useParsedMetafields.d.ts +2 -2
  74. package/dist/esnext/hooks/useParsedMetafields/useParsedMetafields.js +2 -2
  75. package/dist/esnext/hooks/useShopQuery/hooks.d.ts +2 -2
  76. package/dist/esnext/index.d.ts +4 -0
  77. package/dist/esnext/index.js +4 -0
  78. package/dist/esnext/storefront-api-types.d.ts +31 -13
  79. package/dist/esnext/storefront-api-types.js +14 -2
  80. package/dist/esnext/types.d.ts +2 -0
  81. package/dist/esnext/utilities/apiRoutes.d.ts +3 -1
  82. package/dist/esnext/utilities/apiRoutes.js +24 -1
  83. package/dist/esnext/utilities/flattenConnection/flattenConnection.d.ts +1 -1
  84. package/dist/esnext/utilities/flattenConnection/flattenConnection.js +1 -1
  85. package/dist/esnext/utilities/parseMetafieldValue/parseMetafieldValue.d.ts +1 -1
  86. package/dist/esnext/utilities/parseMetafieldValue/parseMetafieldValue.js +1 -1
  87. package/dist/esnext/version.d.ts +1 -1
  88. package/dist/esnext/version.js +1 -1
  89. package/dist/node/entry-server.d.ts +1 -1
  90. package/dist/node/entry-server.js +12 -5
  91. package/dist/node/foundation/Analytics/Analytics.server.js +27 -2
  92. package/dist/node/foundation/AnalyticsErrorBoundary.client.d.ts +4 -0
  93. package/dist/node/foundation/AnalyticsErrorBoundary.client.js +14 -0
  94. package/dist/node/foundation/Router/BrowserRouter.client.js +8 -8
  95. package/dist/node/foundation/ServerPropsProvider/ServerPropsProvider.d.ts +40 -0
  96. package/dist/node/foundation/ServerPropsProvider/ServerPropsProvider.js +101 -0
  97. package/dist/node/foundation/ServerPropsProvider/index.d.ts +2 -0
  98. package/dist/node/foundation/ServerPropsProvider/index.js +6 -0
  99. package/dist/node/foundation/session/session.d.ts +27 -0
  100. package/dist/node/foundation/session/session.js +43 -0
  101. package/dist/node/foundation/useServerProps/use-server-props.d.ts +21 -0
  102. package/dist/node/foundation/useServerProps/use-server-props.js +40 -0
  103. package/dist/node/framework/Hydration/ServerComponentRequest.server.d.ts +2 -0
  104. package/dist/node/framework/plugins/vite-plugin-hydrogen-config.js +25 -12
  105. package/dist/node/storefront-api-types.d.ts +31 -13
  106. package/dist/node/storefront-api-types.js +14 -2
  107. package/dist/node/types.d.ts +2 -0
  108. package/dist/node/utilities/apiRoutes.d.ts +3 -1
  109. package/dist/node/utilities/apiRoutes.js +24 -1
  110. package/dist/node/utilities/flattenConnection/flattenConnection.d.ts +1 -1
  111. package/dist/node/utilities/flattenConnection/flattenConnection.js +1 -1
  112. package/dist/node/utilities/parseMetafieldValue/parseMetafieldValue.d.ts +1 -1
  113. package/dist/node/utilities/parseMetafieldValue/parseMetafieldValue.js +1 -1
  114. package/dist/node/version.d.ts +1 -1
  115. package/dist/node/version.js +1 -1
  116. package/package.json +4 -1
  117. package/dist/esnext/foundation/ServerStateProvider/index.d.ts +0 -2
  118. package/dist/esnext/foundation/ServerStateProvider/index.js +0 -1
  119. package/dist/esnext/foundation/useServerState/index.d.ts +0 -1
  120. package/dist/esnext/foundation/useServerState/index.js +0 -1
  121. package/dist/esnext/foundation/useServerState/use-server-state.d.ts +0 -16
  122. package/dist/esnext/foundation/useServerState/use-server-state.js +0 -20
  123. package/dist/node/foundation/ServerStateProvider/ServerStateProvider.d.ts +0 -30
  124. package/dist/node/foundation/ServerStateProvider/ServerStateProvider.js +0 -77
  125. package/dist/node/foundation/ServerStateProvider/index.d.ts +0 -2
  126. package/dist/node/foundation/ServerStateProvider/index.js +0 -6
  127. package/dist/node/foundation/useServerState/index.d.ts +0 -1
  128. package/dist/node/foundation/useServerState/index.js +0 -5
  129. package/dist/node/foundation/useServerState/use-server-state.d.ts +0 -16
  130. package/dist/node/foundation/useServerState/use-server-state.js +0 -24
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { useImageUrl } from '../../utilities';
3
3
  /**
4
- * The `Video` component renders a `video` for the Storefront API's [Video object](/api/storefront/reference/products/video).
4
+ * The `Video` component renders a `video` for the Storefront API's [Video object](https://shopify.dev/api/storefront/reference/products/video).
5
5
  */
6
6
  export function Video(props) {
7
7
  var _a;
@@ -3,7 +3,7 @@ import React, { Suspense, useState, StrictMode, Fragment, } from 'react';
3
3
  import { hydrateRoot } from 'react-dom/client';
4
4
  import { ErrorBoundary } from 'react-error-boundary';
5
5
  import { useServerResponse } from './framework/Hydration/rsc';
6
- import { ServerStateProvider } from './foundation/ServerStateProvider';
6
+ import { ServerPropsProvider } from './foundation/ServerPropsProvider';
7
7
  const DevTools = React.lazy(() => import('./components/DevTools'));
8
8
  const renderHydrogen = async (ClientWrapper, config) => {
9
9
  const root = document.getElementById('root');
@@ -42,12 +42,12 @@ const renderHydrogen = async (ClientWrapper, config) => {
42
42
  };
43
43
  export default renderHydrogen;
44
44
  function Content({ clientWrapper: ClientWrapper = ({ children }) => children, }) {
45
- const [serverState, setServerState] = useState({
45
+ const [serverProps, setServerProps] = useState({
46
46
  pathname: window.location.pathname,
47
47
  search: window.location.search,
48
48
  });
49
- const response = useServerResponse(serverState);
50
- return (React.createElement(ServerStateProvider, { serverState: serverState, setServerState: setServerState },
49
+ const response = useServerResponse(serverProps);
50
+ return (React.createElement(ServerPropsProvider, { initialServerProps: serverProps, setServerPropsForRsc: setServerProps },
51
51
  React.createElement(ClientWrapper, null, response.readRoot())));
52
52
  }
53
53
  function Error({ error }) {
@@ -19,5 +19,5 @@ interface RequestHandlerOptions {
19
19
  export interface RequestHandler {
20
20
  (request: Request | IncomingMessage, options: RequestHandlerOptions): Promise<Response | undefined>;
21
21
  }
22
- export declare const renderHydrogen: (App: any, { shopifyConfig, routes, serverAnalyticsConnectors }: ServerHandlerConfig) => RequestHandler;
22
+ export declare const renderHydrogen: (App: any, { shopifyConfig, routes, serverAnalyticsConnectors, session, }: ServerHandlerConfig) => RequestHandler;
23
23
  export default renderHydrogen;
@@ -7,7 +7,7 @@ import { ServerComponentResponse } from './framework/Hydration/ServerComponentRe
7
7
  import { ServerComponentRequest } from './framework/Hydration/ServerComponentRequest.server';
8
8
  import { preloadRequestCacheData, ServerRequestProvider, } from './foundation/ServerRequestProvider';
9
9
  import { getApiRouteFromURL, renderApiRoute, getApiRoutes, } from './utilities/apiRoutes';
10
- import { ServerStateProvider } from './foundation/ServerStateProvider';
10
+ import { ServerPropsProvider } from './foundation/ServerPropsProvider';
11
11
  import { isBotUA } from './utilities/bot-ua';
12
12
  import { setContext, setCache } from './framework/runtime';
13
13
  import { setConfig } from './framework/config';
@@ -16,17 +16,20 @@ import { RSC_PATHNAME, EVENT_PATHNAME, EVENT_PATHNAME_REGEX } from './constants'
16
16
  import { stripScriptsFromTemplate } from './utilities/template';
17
17
  import { Analytics } from './foundation/Analytics/Analytics.server';
18
18
  import { ServerAnalyticsRoute } from './foundation/Analytics/ServerAnalyticsRoute.server';
19
+ import { getSyncSessionApi } from './foundation/session/session';
19
20
  const DOCTYPE = '<!DOCTYPE html>';
20
21
  const CONTENT_TYPE = 'Content-Type';
21
22
  const HTML_CONTENT_TYPE = 'text/html; charset=UTF-8';
22
- export const renderHydrogen = (App, { shopifyConfig, routes, serverAnalyticsConnectors }) => {
23
+ export const renderHydrogen = (App, { shopifyConfig, routes, serverAnalyticsConnectors, session, }) => {
23
24
  const handleRequest = async function (rawRequest, options) {
24
25
  const { indexTemplate, streamableResponse, dev, cache, context, nonce, buyerIpHeader, } = options;
25
26
  const request = new ServerComponentRequest(rawRequest);
26
27
  request.ctx.buyerIpHeader = buyerIpHeader;
27
28
  const url = new URL(request.url);
28
29
  const log = getLoggerWithContext(request);
30
+ const sessionApi = session ? session(log) : undefined;
29
31
  const componentResponse = new ServerComponentResponse();
32
+ request.ctx.session = getSyncSessionApi(request, componentResponse, log, sessionApi);
30
33
  /**
31
34
  * Inject the cache & context into the module loader so we can pull it out for subrequests.
32
35
  */
@@ -44,7 +47,7 @@ export const renderHydrogen = (App, { shopifyConfig, routes, serverAnalyticsConn
44
47
  // If it does, only render the API route if the request method is GET
45
48
  if (apiRoute &&
46
49
  (!apiRoute.hasServerComponent || request.method !== 'GET')) {
47
- const apiResponse = await renderApiRoute(request, apiRoute, shopifyConfig);
50
+ const apiResponse = await renderApiRoute(request, apiRoute, shopifyConfig, sessionApi);
48
51
  return apiResponse instanceof Request
49
52
  ? handleRequest(apiResponse, options)
50
53
  : apiResponse;
@@ -131,7 +134,11 @@ async function render(url, { App, routes, request, componentResponse, log, templ
131
134
  headers[CONTENT_TYPE] = HTML_CONTENT_TYPE;
132
135
  html = applyHtmlHead(html, request.ctx.head, template);
133
136
  if (flight) {
134
- html = html.replace('</body>', `${flightContainer({ init: true, nonce, chunk: flight })}</body>`);
137
+ html = html.replace('</body>', () => `${flightContainer({
138
+ init: true,
139
+ nonce,
140
+ chunk: flight,
141
+ })}</body>`);
135
142
  }
136
143
  postRequestTasks('ssr', status, request, componentResponse);
137
144
  return new Response(html, {
@@ -410,7 +417,7 @@ function buildAppSSR({ App, state, request, response, log, routes }, htmlOptions
410
417
  const RscConsumer = () => rscResponse.readRoot();
411
418
  const AppSSR = (React.createElement(Html, { ...htmlOptions },
412
419
  React.createElement(ServerRequestProvider, { request: request, isRSC: false },
413
- React.createElement(ServerStateProvider, { serverState: state, setServerState: () => { } },
420
+ React.createElement(ServerPropsProvider, { initialServerProps: state, setServerPropsForRsc: () => { } },
414
421
  React.createElement(PreloadQueries, { request: request },
415
422
  React.createElement(Suspense, { fallback: null },
416
423
  React.createElement(RscConsumer, null)),
@@ -1,7 +1,8 @@
1
- import React from 'react';
1
+ import * as React from 'react';
2
2
  import { useServerAnalytics } from './hook';
3
3
  import { Analytics as AnalyticsClient } from './Analytics.client';
4
4
  import { useServerRequest } from '../ServerRequestProvider';
5
+ import AnalyticsErrorBoundary from '../AnalyticsErrorBoundary.client';
5
6
  const DELAY_KEY = 'analytics-delay';
6
7
  export function Analytics() {
7
8
  const cache = useServerRequest().ctx.cache;
@@ -34,5 +35,6 @@ export function Analytics() {
34
35
  }
35
36
  });
36
37
  const analyticsData = useServerAnalytics();
37
- return React.createElement(AnalyticsClient, { analyticsDataFromServer: analyticsData });
38
+ return (React.createElement(AnalyticsErrorBoundary, null,
39
+ React.createElement(AnalyticsClient, { analyticsDataFromServer: analyticsData })));
38
40
  }
@@ -0,0 +1,4 @@
1
+ import type { ReactNode } from 'react';
2
+ export default function AnalyticsErrorBoundary({ children, }: {
3
+ children: ReactNode;
4
+ }): JSX.Element;
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { ErrorBoundary } from 'react-error-boundary';
3
+ export default function AnalyticsErrorBoundary({ children, }) {
4
+ // Analytics fail to load, most likely due to an ad blocker
5
+ return (React.createElement(ErrorBoundary, { fallbackRender: () => {
6
+ return null;
7
+ } }, children));
8
+ }
@@ -0,0 +1,48 @@
1
+ export declare type CookieOptions = {
2
+ /** Whether to secure the cookie so that the browser only sends it over HTTPS. Some
3
+ * browsers [don't work with secure cookies on localhost](https://owasp.org/www-community/controls/SecureCookieAttribute).
4
+ */
5
+ httpOnly?: boolean;
6
+ /** Whether to secure the cookie so that [client JavaScript is unable to read it](https://owasp.org/www-community/HttpOnly).
7
+ */
8
+ secure?: boolean;
9
+ /** Declares that the cookie should be restricted to a first-party
10
+ * or [same-site](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) context.
11
+ */
12
+ sameSite?: 'Lax' | 'Strict' | 'None';
13
+ /** Tells the browser that the cookie should only be sent to the server if it's
14
+ * within the [defined path](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#path_attribute).
15
+ */
16
+ path?: string;
17
+ /** [A date in which the cookie will expire](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#define_the_lifetime_of_a_cookie).
18
+ * If the date is in the past, then the browser will remove the cookie.
19
+ */
20
+ expires?: Date;
21
+ /** Secures the cookie so that it's only used on [specific domains](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#domain_attribute).
22
+ */
23
+ domain?: string;
24
+ /** The [number of seconds until the cookie expires](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#max-agenumber).
25
+ * `maxAge` takes precedence over `expires` if both are defined.
26
+ */
27
+ maxAge?: number;
28
+ };
29
+ /** The `Cookie` component helps you build your own custom cookie and session implementations. All
30
+ * [Hydrogen session storage mechanisms](https://shopify.dev/custom-storefronts/hydrogen/framework/sessions#types-of-session-storage) use the
31
+ * same configuration options as what's available in `Cookie`.
32
+ */
33
+ export declare class Cookie {
34
+ /** The name of the cookie stored in the browser. */
35
+ name: string;
36
+ /** An optional object to configure [how the cookie is persisted in the browser](https://shopify.dev/api/hydrogen/components/framework/cookie#cookie-options). */
37
+ options?: CookieOptions;
38
+ data: Record<string, any>;
39
+ constructor(name: string, options?: CookieOptions);
40
+ parse(cookie: string): Record<string, any>;
41
+ set(key: string, value: string): void;
42
+ setAll(data: Record<string, string>): void;
43
+ serialize(): string;
44
+ destroy(): string;
45
+ get expires(): number;
46
+ setSessionid(sid: string): void;
47
+ getSessionId(request: Request): string | null;
48
+ }
@@ -0,0 +1,66 @@
1
+ import { parse, stringify as stringifyCookie } from 'worktop/cookie';
2
+ import { log } from '../../utilities/log';
3
+ const reservedCookieNames = ['mac', 'user_session_id'];
4
+ /** The `Cookie` component helps you build your own custom cookie and session implementations. All
5
+ * [Hydrogen session storage mechanisms](https://shopify.dev/custom-storefronts/hydrogen/framework/sessions#types-of-session-storage) use the
6
+ * same configuration options as what's available in `Cookie`.
7
+ */
8
+ export class Cookie {
9
+ constructor(name, options = {}) {
10
+ if (reservedCookieNames.includes(name)) {
11
+ log.warn(`Warning "${name}" is a reserved cookie name by oxygen!`);
12
+ }
13
+ this.options = options;
14
+ this.options = {
15
+ ...this.options,
16
+ expires:
17
+ // maxAge takes precedence
18
+ typeof options.maxAge !== 'undefined'
19
+ ? new Date(Date.now() + options.maxAge * 1000)
20
+ : options.expires
21
+ ? options.expires
22
+ : new Date(Date.now() + 604800000), // default one week
23
+ };
24
+ this.name = name;
25
+ this.data = {};
26
+ }
27
+ parse(cookie) {
28
+ try {
29
+ const data = JSON.parse(parse(cookie)[this.name]);
30
+ this.data = data;
31
+ }
32
+ catch (e) {
33
+ // failure to parse cookie
34
+ }
35
+ return this.data;
36
+ }
37
+ set(key, value) {
38
+ this.data[key] = value;
39
+ }
40
+ setAll(data) {
41
+ this.data = data;
42
+ }
43
+ serialize() {
44
+ return stringifyCookie(this.name, JSON.stringify(this.data), this.options);
45
+ }
46
+ destroy() {
47
+ this.data = {};
48
+ return stringifyCookie(this.name, '', {
49
+ ...this.options,
50
+ expires: new Date(0),
51
+ });
52
+ }
53
+ get expires() {
54
+ return this.options.expires.getTime();
55
+ }
56
+ setSessionid(sid) {
57
+ return this.set('sid', sid);
58
+ }
59
+ getSessionId(request) {
60
+ const cookieValue = request.headers.get('cookie');
61
+ if (cookieValue) {
62
+ return this.parse(cookieValue).sid;
63
+ }
64
+ return null;
65
+ }
66
+ }
@@ -0,0 +1,5 @@
1
+ import type { SessionStorageAdapter } from '../session/session';
2
+ import type { CookieOptions } from '../Cookie/Cookie';
3
+ /** The `CookieSessionStorage` component is the default session storage mechanism for Hydrogen.
4
+ */
5
+ export declare const CookieSessionStorage: (name: string, options: CookieOptions) => () => SessionStorageAdapter;
@@ -0,0 +1,31 @@
1
+ import { Cookie } from '../Cookie/Cookie';
2
+ /** The `CookieSessionStorage` component is the default session storage mechanism for Hydrogen.
3
+ */
4
+ export const CookieSessionStorage = function (
5
+ /** The name of the cookie stored in the browser. */
6
+ name,
7
+ /** An optional object to configure [how the cookie is persisted in the browser](https://shopify.dev/api/hydrogen/components/framework/cookie#cookie-options). */
8
+ options) {
9
+ return function () {
10
+ const cookie = new Cookie(name, options);
11
+ let parsed = false;
12
+ return {
13
+ async get(request) {
14
+ if (!parsed) {
15
+ const cookieValue = request.headers.get('cookie');
16
+ cookie.parse(cookieValue || '');
17
+ parsed = true;
18
+ }
19
+ return cookie.data;
20
+ },
21
+ async set(request, value) {
22
+ cookie.setAll(value);
23
+ return cookie.serialize();
24
+ },
25
+ async destroy(request) {
26
+ // @todo - set expires for Date in past
27
+ return cookie.destroy();
28
+ },
29
+ };
30
+ };
31
+ };
@@ -0,0 +1,6 @@
1
+ import type { SessionStorageAdapter } from '../session/session';
2
+ import type { CookieOptions } from '../Cookie/Cookie';
3
+ import { Logger } from '../../utilities/log';
4
+ /** The `FileSessionStorage` component persists session data to the file system.
5
+ */
6
+ export declare const FileSessionStorage: (name: string, dir: string, cookieOptions: CookieOptions) => (log: Logger) => SessionStorageAdapter;
@@ -0,0 +1,148 @@
1
+ import { Cookie } from '../Cookie/Cookie';
2
+ import { v4 as uid } from 'uuid';
3
+ import path from 'path';
4
+ import { promises as fsp } from 'fs';
5
+ async function wait() {
6
+ return new Promise((resolve) => setTimeout(resolve));
7
+ }
8
+ let writingLock = false;
9
+ /**
10
+ * Concurrent requests with the same session can write interfere
11
+ * writing at the same file, which crashes the process. This locks
12
+ * so that only one file can be written at a time.
13
+ *
14
+ * A better solution would be to have a lock only for the same session.
15
+ */
16
+ async function startFileLock(promise) {
17
+ if (!writingLock) {
18
+ writingLock = true;
19
+ await promise();
20
+ writingLock = false;
21
+ }
22
+ else {
23
+ await wait();
24
+ await startFileLock(promise);
25
+ }
26
+ }
27
+ /** The `FileSessionStorage` component persists session data to the file system.
28
+ */
29
+ export const FileSessionStorage = function (
30
+ /** The name of the cookie stored in the browser. */
31
+ name,
32
+ /** A directory to store the session files within. Each session is stored in a separate file on the file system. */
33
+ dir,
34
+ /** An optional object to configure [how the cookie is persisted in the browser](https://shopify.dev/api/hydrogen/components/framework/cookie#cookie-options). */
35
+ cookieOptions) {
36
+ return function (log) {
37
+ const cookie = new Cookie(name, cookieOptions);
38
+ let data;
39
+ return {
40
+ async get(request) {
41
+ if (data)
42
+ return data;
43
+ const sid = cookie.getSessionId(request) || uid();
44
+ const file = getSessionFile(dir, sid);
45
+ const fileContents = await getFile(file, cookie.expires, log);
46
+ data = fileContents.data;
47
+ return data;
48
+ },
49
+ async set(request, value) {
50
+ const sid = cookie.getSessionId(request) || uid();
51
+ const file = getSessionFile(dir, sid);
52
+ await writeFile(file, value, cookie.expires);
53
+ data = value;
54
+ cookie.setSessionid(sid);
55
+ return cookie.serialize();
56
+ },
57
+ async destroy(request) {
58
+ const sid = cookie.getSessionId(request);
59
+ if (sid) {
60
+ const file = getSessionFile(dir, sid);
61
+ await deleteFile(file);
62
+ }
63
+ data = undefined;
64
+ // @todo - set expires for Date in past
65
+ return cookie.destroy();
66
+ },
67
+ };
68
+ };
69
+ };
70
+ async function getFile(file, expires, log) {
71
+ let content = null;
72
+ const defaultFileContent = {
73
+ data: {},
74
+ expires,
75
+ };
76
+ await startFileLock(async () => {
77
+ try {
78
+ const textContent = await fsp.readFile(file, { encoding: 'utf-8' });
79
+ try {
80
+ content = JSON.parse(textContent);
81
+ }
82
+ catch (error) {
83
+ log.warn(`Cannot parse existing session file: ${file}`);
84
+ content = defaultFileContent;
85
+ }
86
+ if (content.expires < new Date().getTime() ||
87
+ content === defaultFileContent) {
88
+ await fsp.unlink(file);
89
+ await fsp.writeFile(file, JSON.stringify(defaultFileContent), {
90
+ encoding: 'utf-8',
91
+ flag: 'wx',
92
+ });
93
+ content = defaultFileContent;
94
+ }
95
+ }
96
+ catch (error) {
97
+ if (error.code !== 'ENOENT')
98
+ throw error;
99
+ await fsp.mkdir(path.dirname(file), { recursive: true });
100
+ await fsp.writeFile(file, JSON.stringify(defaultFileContent), {
101
+ encoding: 'utf-8',
102
+ flag: 'wx',
103
+ });
104
+ }
105
+ });
106
+ return content ? content : defaultFileContent;
107
+ }
108
+ async function writeFile(file, data, expires) {
109
+ const content = {
110
+ data,
111
+ expires,
112
+ };
113
+ await startFileLock(async () => {
114
+ try {
115
+ await fsp.mkdir(path.dirname(file), { recursive: true });
116
+ }
117
+ catch (error) {
118
+ // directory already exists
119
+ if (error.code !== 'EEXIST')
120
+ throw error;
121
+ }
122
+ try {
123
+ await fsp.unlink(file);
124
+ }
125
+ catch (error) {
126
+ if (error.code !== 'ENOENT')
127
+ throw error;
128
+ }
129
+ await fsp.writeFile(file, JSON.stringify(content), {
130
+ encoding: 'utf-8',
131
+ flag: 'wx',
132
+ });
133
+ });
134
+ }
135
+ async function deleteFile(file) {
136
+ try {
137
+ await startFileLock(async () => {
138
+ await fsp.unlink(file);
139
+ });
140
+ }
141
+ catch (error) {
142
+ if (error.code !== 'ENOENT')
143
+ throw error;
144
+ }
145
+ }
146
+ function getSessionFile(dir, sid) {
147
+ return path.join(dir, sid.slice(0, 3), sid.slice(3));
148
+ }
@@ -0,0 +1,5 @@
1
+ import type { SessionStorageAdapter } from '../session/session';
2
+ import type { CookieOptions } from '../Cookie/Cookie';
3
+ /** The `MemorySessionStorage` component stores session data within Hydrogen runtime memory.
4
+ */
5
+ export declare const MemorySessionStorage: (name: string, options: CookieOptions) => () => SessionStorageAdapter;
@@ -0,0 +1,53 @@
1
+ import { Cookie } from '../Cookie/Cookie';
2
+ import { v4 as uid } from 'uuid';
3
+ /** The `MemorySessionStorage` component stores session data within Hydrogen runtime memory.
4
+ */
5
+ export const MemorySessionStorage = function (
6
+ /** The name of the cookie stored in the browser. */
7
+ name,
8
+ /** An optional object to configure [how the cookie is persisted in the browser](https://shopify.dev/api/hydrogen/components/framework/cookie#cookie-options). */
9
+ options) {
10
+ const sessions = new Map();
11
+ return function () {
12
+ const cookie = new Cookie(name, options);
13
+ return {
14
+ async get(request) {
15
+ const sid = cookie.getSessionId(request);
16
+ let sessionData;
17
+ if (sid && sessions.has(sid)) {
18
+ const { expires, data } = sessions.get(sid);
19
+ if (expires < new Date().getTime()) {
20
+ sessions.delete(sid);
21
+ sessionData = {};
22
+ }
23
+ else {
24
+ sessionData = data;
25
+ }
26
+ }
27
+ else {
28
+ sessionData = {};
29
+ }
30
+ return sessionData;
31
+ },
32
+ async set(request, value) {
33
+ let sid = cookie.getSessionId(request);
34
+ if (!sid) {
35
+ sid = uid();
36
+ }
37
+ sessions.set(sid, {
38
+ data: value,
39
+ expires: cookie.expires,
40
+ });
41
+ cookie.setSessionid(sid);
42
+ return cookie.serialize();
43
+ },
44
+ async destroy(request) {
45
+ const sid = cookie.getSessionId(request);
46
+ if (sid) {
47
+ sessions.delete(sid);
48
+ }
49
+ return cookie.destroy();
50
+ },
51
+ };
52
+ };
53
+ };
@@ -1,7 +1,7 @@
1
1
  import { createBrowserHistory } from 'history';
2
2
  import React, { createContext, useContext, useMemo, useState, useEffect, useLayoutEffect, useCallback, } from 'react';
3
3
  import { META_ENV_SSR } from '../ssr-interop';
4
- import { useServerState } from '../useServerState';
4
+ import { useInternalServerProps } from '../useServerProps/use-server-props';
5
5
  export const RouterContext = createContext({});
6
6
  let isFirstLoad = true;
7
7
  const positions = {};
@@ -10,12 +10,12 @@ export const BrowserRouter = ({ history: pHistory, children, }) => {
10
10
  return React.createElement(React.Fragment, null, children);
11
11
  const history = useMemo(() => pHistory || createBrowserHistory(), [pHistory]);
12
12
  const [location, setLocation] = useState(history.location);
13
- const { pending, serverState, setServerState } = useServerState();
14
- useScrollRestoration(location, pending, serverState);
13
+ const { pending, locationServerProps, setLocationServerProps } = useInternalServerProps();
14
+ useScrollRestoration(location, pending, locationServerProps);
15
15
  useLayoutEffect(() => {
16
16
  const unlisten = history.listen(({ location: newLocation }) => {
17
17
  positions[location.key] = window.scrollY;
18
- setServerState({
18
+ setLocationServerProps({
19
19
  pathname: newLocation.pathname,
20
20
  search: location.search,
21
21
  });
@@ -49,7 +49,7 @@ function useBeforeUnload(callback) {
49
49
  };
50
50
  }, [callback]);
51
51
  }
52
- function useScrollRestoration(location, pending, serverState) {
52
+ function useScrollRestoration(location, pending, serverProps) {
53
53
  /**
54
54
  * Browsers have an API for scroll restoration. We wait for the page to load first,
55
55
  * in case the browser is able to restore scroll position automatically, and then
@@ -77,8 +77,8 @@ function useScrollRestoration(location, pending, serverState) {
77
77
  * location pointer and serverState match, and pending is false, to do any scrolling.
78
78
  */
79
79
  const finishedNavigating = !pending &&
80
- location.pathname === serverState.pathname &&
81
- location.search === serverState.search;
80
+ location.pathname === serverProps.pathname &&
81
+ location.search === serverProps.search;
82
82
  if (!finishedNavigating) {
83
83
  return;
84
84
  }
@@ -97,5 +97,5 @@ function useScrollRestoration(location, pending, serverState) {
97
97
  }
98
98
  // Scroll to the top of new pages
99
99
  window.scrollTo(0, 0);
100
- }, [location, pending, serverState]);
100
+ }, [location, pending, serverProps]);
101
101
  }
@@ -0,0 +1,40 @@
1
+ import React, { ReactNode } from 'react';
2
+ declare global {
3
+ var __DEV__: boolean;
4
+ }
5
+ export interface LocationServerProps {
6
+ pathname: string;
7
+ search: string;
8
+ }
9
+ export interface ServerProps {
10
+ [key: string]: any;
11
+ }
12
+ declare type ServerPropsSetterInput = ((prev: ServerProps) => Partial<ServerProps>) | Partial<ServerProps> | string;
13
+ export interface ServerPropsSetter {
14
+ (input: ServerPropsSetterInput, propValue?: any): void;
15
+ }
16
+ interface ProposedServerPropsSetter {
17
+ (input: ServerPropsSetterInput, propValue?: any): LocationServerProps;
18
+ }
19
+ interface BaseServerPropsContextValue {
20
+ pending: boolean;
21
+ }
22
+ export interface InternalServerPropsContextValue extends BaseServerPropsContextValue {
23
+ setLocationServerProps: ServerPropsSetter;
24
+ setServerProps: ServerPropsSetter;
25
+ serverProps: ServerProps;
26
+ locationServerProps: LocationServerProps;
27
+ getProposedLocationServerProps: ProposedServerPropsSetter;
28
+ }
29
+ export interface ServerPropsContextValue extends BaseServerPropsContextValue {
30
+ serverProps: ServerProps;
31
+ setServerProps: ServerPropsSetter;
32
+ }
33
+ export declare const ServerPropsContext: React.Context<InternalServerPropsContextValue>;
34
+ interface ServerPropsProviderProps {
35
+ initialServerProps: LocationServerProps;
36
+ setServerPropsForRsc: React.Dispatch<React.SetStateAction<LocationServerProps>>;
37
+ children: ReactNode;
38
+ }
39
+ export declare function ServerPropsProvider({ initialServerProps, setServerPropsForRsc, children, }: ServerPropsProviderProps): JSX.Element;
40
+ export {};