@shopify/hydrogen 0.13.0 → 0.13.1

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 (29) hide show
  1. package/CHANGELOG.md +19 -5
  2. package/dist/esnext/entry-server.d.ts +1 -0
  3. package/dist/esnext/entry-server.js +2 -1
  4. package/dist/esnext/foundation/ServerRequestProvider/ServerRequestProvider.js +1 -1
  5. package/dist/esnext/foundation/useQuery/hooks.d.ts +3 -0
  6. package/dist/esnext/foundation/useQuery/hooks.js +8 -2
  7. package/dist/esnext/framework/Hydration/ServerComponentRequest.server.d.ts +7 -0
  8. package/dist/esnext/framework/Hydration/ServerComponentRequest.server.js +23 -7
  9. package/dist/esnext/framework/cache/in-memory.d.ts +1 -0
  10. package/dist/esnext/framework/cache/in-memory.js +15 -5
  11. package/dist/esnext/hooks/useShopQuery/hooks.js +19 -10
  12. package/dist/esnext/platforms/node.d.ts +2 -3
  13. package/dist/esnext/platforms/node.js +5 -3
  14. package/dist/esnext/platforms/worker.js +2 -1
  15. package/dist/esnext/utilities/apiRoutes.js +3 -1
  16. package/dist/esnext/utilities/flattenConnection/flattenConnection.js +2 -5
  17. package/dist/esnext/version.d.ts +1 -1
  18. package/dist/esnext/version.js +1 -1
  19. package/dist/node/entry-server.d.ts +1 -0
  20. package/dist/node/entry-server.js +2 -1
  21. package/dist/node/foundation/ServerRequestProvider/ServerRequestProvider.js +1 -1
  22. package/dist/node/framework/Hydration/ServerComponentRequest.server.d.ts +7 -0
  23. package/dist/node/framework/Hydration/ServerComponentRequest.server.js +23 -7
  24. package/dist/node/framework/cache/in-memory.d.ts +1 -0
  25. package/dist/node/framework/cache/in-memory.js +15 -5
  26. package/dist/node/utilities/apiRoutes.js +3 -1
  27. package/dist/node/version.d.ts +1 -1
  28. package/dist/node/version.js +1 -1
  29. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1008](https://github.com/Shopify/hydrogen/pull/1008) [`ca1de82b`](https://github.com/Shopify/hydrogen/commit/ca1de82bc38c1c02caa451fb52065da499555e6f) Thanks [@frandiox](https://github.com/frandiox)! - Allow passing `cache` parameter to `createServer` in Node entry.
8
+
9
+ * [#997](https://github.com/Shopify/hydrogen/pull/997) [`fffdc08f`](https://github.com/Shopify/hydrogen/commit/fffdc08f87f71592352a2eb67a63e80704054db2) Thanks [@frandiox](https://github.com/frandiox)! - Allow empty array values in flattenConnection utility.
10
+
11
+ - [#1007](https://github.com/Shopify/hydrogen/pull/1007) [`7cfca7b0`](https://github.com/Shopify/hydrogen/commit/7cfca7b09289e028a463ababb51e69b4e3943d94) Thanks [@scottdixon](https://github.com/scottdixon)! - Fix API index routes https://github.com/Shopify/hydrogen/issues/562
12
+
13
+ * [#1000](https://github.com/Shopify/hydrogen/pull/1000) [`6d0d5068`](https://github.com/Shopify/hydrogen/commit/6d0d50686029c3d66d9dc0ceb0b5f71456c7b19e) Thanks [@frandiox](https://github.com/frandiox)! - Do not cache Storefront API responses that contain GraphQL errors.
14
+
15
+ - [#1003](https://github.com/Shopify/hydrogen/pull/1003) [`d8a9c929`](https://github.com/Shopify/hydrogen/commit/d8a9c9290aaf7c9d058b2c08567294822bea5396) Thanks [@jplhomer](https://github.com/jplhomer)! - Update useShopQuery to accept a custom Storefront API secret token, and forward the Buyer IP.
16
+
3
17
  ## 0.13.0
4
18
 
5
19
  ### Minor Changes
@@ -29,7 +43,7 @@
29
43
 
30
44
  These fragments have been removed to reduce the chances of over-fetching (in other words, querying for fields you don't use) in your GraphQL queries. Please refer to the [Storefront API documentation](https://shopify.dev/api/storefront) for information and guides.
31
45
 
32
- * [#912](https://github.com/Shopify/hydrogen/pull/912) [`de0e0d6a`](https://github.com/Shopify/hydrogen/commit/de0e0d6a6652463243ee09013cd30830ce2a246a) Thanks [@blittle](https://github.com/blittle)! - Change the country selector to lazy load available countries. The motivation to do so is that a _lot_ of countries come with the starter template. The problem is 1) the graphql query to fetch them all is relatively slow and 2) all of them get serialized to the browser in each RSC response.
46
+ * [#912](https://github.com/Shopify/hydrogen/pull/912) [`de0e0d6a`](https://github.com/Shopify/hydrogen/commit/de0e0d6a6652463243ee09013cd30830ce2a246a) Thanks [@blittle](https://github.com/blittle)! - Change the country selector to lazy load available countries. The motivation to do so is that a _lot_ of countries come with the Demo Store template. The problem is 1) the graphql query to fetch them all is relatively slow and 2) all of them get serialized to the browser in each RSC response.
33
47
 
34
48
  This change removes `availableCountries` from the `LocalizationProvider`. As a result, the `useAvailableCountries` hook is also gone. Instead, the available countries are loaded on demand from an API route.
35
49
 
@@ -79,7 +93,7 @@
79
93
  }, []);
80
94
  ```
81
95
 
82
- See an example on how this could be done inside the Hydrogen Example Template [country selector](https://github.com/Shopify/hydrogen/blob/v1.x-2022-07/examples/template-hydrogen-default/src/components/CountrySelector.client.jsx)
96
+ See an example on how this could be done inside the Demo Store template [country selector](https://github.com/Shopify/hydrogen/blob/v1.x-2022-07/examples/template-hydrogen-default/src/components/CountrySelector.client.jsx)
83
97
 
84
98
  - [#698](https://github.com/Shopify/hydrogen/pull/698) [`6f30b9a1`](https://github.com/Shopify/hydrogen/commit/6f30b9a1327f06d648a01dd94d539c7dcb3061e0) Thanks [@jplhomer](https://github.com/jplhomer)! - Basic end-to-end tests have been added to the default Hydrogen template. You can run tests in development:
85
99
 
@@ -239,7 +253,7 @@
239
253
 
240
254
  - [#981](https://github.com/Shopify/hydrogen/pull/981) [`8dda8a86`](https://github.com/Shopify/hydrogen/commit/8dda8a860bc1cf58511756b6fff999fb7caa6081) Thanks [@michenly](https://github.com/michenly)! - Fix useUrl() when it is in RSC mode
241
255
 
242
- * [#965](https://github.com/Shopify/hydrogen/pull/965) [`cdad13ed`](https://github.com/Shopify/hydrogen/commit/cdad13ed85ff17b84981367f39c7d2fe45e72dcf) Thanks [@blittle](https://github.com/blittle)! - Fix server redirects to work properly with RSC responses. For example, the redirect component within the starter template needs to change:
256
+ * [#965](https://github.com/Shopify/hydrogen/pull/965) [`cdad13ed`](https://github.com/Shopify/hydrogen/commit/cdad13ed85ff17b84981367f39c7d2fe45e72dcf) Thanks [@blittle](https://github.com/blittle)! - Fix server redirects to work properly with RSC responses. For example, the redirect component within the Demo Store template needs to change:
243
257
 
244
258
  ```diff
245
259
  export default function Redirect({response}) {
@@ -289,7 +303,7 @@
289
303
 
290
304
  ### Minor Changes
291
305
 
292
- - [`8271be8`](https://github.com/Shopify/hydrogen/commit/8271be83331c99f27a258e6532983da4fe4f0b5b) Thanks [@michenly](https://github.com/michenly)! - Export Seo components Fragement and use them in the starter template.
306
+ - [`8271be8`](https://github.com/Shopify/hydrogen/commit/8271be83331c99f27a258e6532983da4fe4f0b5b) Thanks [@michenly](https://github.com/michenly)! - Export Seo components Fragement and use them in the Demo Store template.
293
307
 
294
308
  * [#827](https://github.com/Shopify/hydrogen/pull/827) [`745e8c0`](https://github.com/Shopify/hydrogen/commit/745e8c0a87a7c41803934565e5a756295ff629c2) Thanks [@michenly](https://github.com/michenly)! - Move any static `Fragment` properties on components to the entry point `@shopify/hydrogen/fragments`.
295
309
  The migration diff are as follows:
@@ -925,7 +939,7 @@ function SomeComponent() {
925
939
 
926
940
  ### Fixed
927
941
 
928
- - Starter template GalleryPreview unique key warning
942
+ - Demo Store template GalleryPreview unique key warning
929
943
  - Mitigation for upcoming breaking minor Vite update
930
944
 
931
945
  ## 0.2.0 - 2021-10-08
@@ -14,6 +14,7 @@ interface RequestHandlerOptions {
14
14
  dev?: boolean;
15
15
  context?: RuntimeContext;
16
16
  nonce?: string;
17
+ buyerIpHeader?: string;
17
18
  }
18
19
  export interface RequestHandler {
19
20
  (request: Request | IncomingMessage, options: RequestHandlerOptions): Promise<Response | undefined>;
@@ -19,8 +19,9 @@ const DOCTYPE = '<!DOCTYPE html>';
19
19
  const CONTENT_TYPE = 'Content-Type';
20
20
  const HTML_CONTENT_TYPE = 'text/html; charset=UTF-8';
21
21
  export const renderHydrogen = (App, { shopifyConfig, routes }) => {
22
- const handleRequest = async function (rawRequest, { indexTemplate, streamableResponse, dev, cache, context, nonce }) {
22
+ const handleRequest = async function (rawRequest, { indexTemplate, streamableResponse, dev, cache, context, nonce, buyerIpHeader, }) {
23
23
  const request = new ServerComponentRequest(rawRequest);
24
+ request.ctx.buyerIpHeader = buyerIpHeader;
24
25
  const url = new URL(request.url);
25
26
  const log = getLoggerWithContext(request);
26
27
  const componentResponse = new ServerComponentResponse();
@@ -30,7 +30,7 @@ export function useServerRequest() {
30
30
  const cache = React.unstable_getCacheForType(requestCacheRSC);
31
31
  request = cache ? cache.get(requestCacheRSC.key) : null;
32
32
  }
33
- catch (error) {
33
+ catch (_a) {
34
34
  // If RSC cache failed it means this is not an RSC request.
35
35
  // Try getting SSR context instead:
36
36
  request = useContext(RequestContextSSR);
@@ -9,6 +9,9 @@ export interface HydrogenUseQueryOptions {
9
9
  * to preload the query for all requests.
10
10
  */
11
11
  preload?: PreloadOptions;
12
+ /** A function that inspects the response body to determine if it should be cached.
13
+ */
14
+ shouldCacheResponse?: (body: any) => boolean;
12
15
  }
13
16
  /**
14
17
  * The `useQuery` hook executes an asynchronous operation like `fetch` in a way that
@@ -32,9 +32,11 @@ queryOptions) {
32
32
  return useRequestCacheData(withCacheIdKey, fetcher);
33
33
  }
34
34
  function cachedQueryFnBuilder(key, queryFn, queryOptions) {
35
+ var _a;
35
36
  const resolvedQueryOptions = {
36
37
  ...(queryOptions !== null && queryOptions !== void 0 ? queryOptions : {}),
37
38
  };
39
+ const shouldCacheResponse = (_a = queryOptions === null || queryOptions === void 0 ? void 0 : queryOptions.shouldCacheResponse) !== null && _a !== void 0 ? _a : (() => true);
38
40
  /**
39
41
  * Attempt to read the query from cache. If it doesn't exist or if it's stale, regenerate it.
40
42
  */
@@ -64,7 +66,9 @@ function cachedQueryFnBuilder(key, queryFn, queryOptions) {
64
66
  await setItemInCache(lockKey, true);
65
67
  try {
66
68
  const output = await generateNewOutput();
67
- await setItemInCache(key, output, resolvedQueryOptions === null || resolvedQueryOptions === void 0 ? void 0 : resolvedQueryOptions.cache);
69
+ if (shouldCacheResponse(output)) {
70
+ await setItemInCache(key, output, resolvedQueryOptions === null || resolvedQueryOptions === void 0 ? void 0 : resolvedQueryOptions.cache);
71
+ }
68
72
  }
69
73
  catch (e) {
70
74
  log.error(`Error generating async response: ${e.message}`);
@@ -80,7 +84,9 @@ function cachedQueryFnBuilder(key, queryFn, queryOptions) {
80
84
  /**
81
85
  * Important: Do this async
82
86
  */
83
- runDelayedFunction(async () => await setItemInCache(key, newOutput, resolvedQueryOptions === null || resolvedQueryOptions === void 0 ? void 0 : resolvedQueryOptions.cache));
87
+ if (shouldCacheResponse(newOutput)) {
88
+ runDelayedFunction(() => setItemInCache(key, newOutput, resolvedQueryOptions === null || resolvedQueryOptions === void 0 ? void 0 : resolvedQueryOptions.cache));
89
+ }
84
90
  collectQueryCacheControlHeaders(request, key, generateSubRequestCacheControlHeader(resolvedQueryOptions === null || resolvedQueryOptions === void 0 ? void 0 : resolvedQueryOptions.cache));
85
91
  return newOutput;
86
92
  }
@@ -35,6 +35,7 @@ export declare class ServerComponentRequest extends Request {
35
35
  queryTimings: Array<QueryTiming>;
36
36
  preloadQueries: PreloadQueriesByURL;
37
37
  router: RouterContextData;
38
+ buyerIpHeader?: string;
38
39
  [key: string]: any;
39
40
  };
40
41
  constructor(input: any);
@@ -44,4 +45,10 @@ export declare class ServerComponentRequest extends Request {
44
45
  savePreloadQuery(query: PreloadQueryEntry): void;
45
46
  getPreloadQueries(): PreloadQueriesByURL | undefined;
46
47
  savePreloadQueries(): void;
48
+ /**
49
+ * Buyer IP varies by hosting provider and runtime. The developer should provide this
50
+ * as an argument to the `handleRequest` function for their runtime.
51
+ * Defaults to `x-forwarded-for` header value.
52
+ */
53
+ getBuyerIp(): string | null;
47
54
  }
@@ -25,13 +25,7 @@ export class ServerComponentRequest extends Request {
25
25
  super(input, init);
26
26
  }
27
27
  else {
28
- super(getUrlFromNodeRequest(input), {
29
- headers: new Headers(input.headers),
30
- method: input.method,
31
- body: input.method !== 'GET' && input.method !== 'HEAD'
32
- ? input.body
33
- : undefined,
34
- });
28
+ super(getUrlFromNodeRequest(input), getInitFromNodeRequest(input));
35
29
  }
36
30
  this.time = getTime();
37
31
  this.id = generateId();
@@ -85,6 +79,15 @@ export class ServerComponentRequest extends Request {
85
79
  savePreloadQueries() {
86
80
  preloadCache.set(this.preloadURL, this.ctx.preloadQueries);
87
81
  }
82
+ /**
83
+ * Buyer IP varies by hosting provider and runtime. The developer should provide this
84
+ * as an argument to the `handleRequest` function for their runtime.
85
+ * Defaults to `x-forwarded-for` header value.
86
+ */
87
+ getBuyerIp() {
88
+ var _a;
89
+ return this.headers.get((_a = this.ctx.buyerIpHeader) !== null && _a !== void 0 ? _a : 'x-forwarded-for');
90
+ }
88
91
  }
89
92
  function mergeMapEntries(map1, map2) {
90
93
  map2 && map2.forEach((v, k) => map1.set(k, v));
@@ -112,3 +115,16 @@ function getUrlFromNodeRequest(request) {
112
115
  const secure = request.headers['x-forwarded-proto'] === 'https';
113
116
  return new URL(`${secure ? 'https' : 'http'}://${request.headers.host + url}`).toString();
114
117
  }
118
+ function getInitFromNodeRequest(request) {
119
+ const init = {
120
+ headers: new Headers(request.headers),
121
+ method: request.method,
122
+ body: request.method !== 'GET' && request.method !== 'HEAD'
123
+ ? request.body
124
+ : undefined,
125
+ };
126
+ if (!init.headers.has('x-forwarded-for')) {
127
+ init.headers.set('x-forwarded-for', request.socket.remoteAddress);
128
+ }
129
+ return init;
130
+ }
@@ -8,4 +8,5 @@ export declare class InMemoryCache {
8
8
  put(request: Request, response: Response): void;
9
9
  match(request: Request): Response | undefined;
10
10
  delete(request: Request): void;
11
+ keys(request?: Request): Promise<Request[]>;
11
12
  }
@@ -15,16 +15,17 @@ export class InMemoryCache {
15
15
  });
16
16
  }
17
17
  match(request) {
18
+ var _a, _b;
18
19
  const match = this.store.get(request.url);
19
20
  if (!match) {
20
21
  logCacheApiStatus('MISS', request.url);
21
22
  return;
22
23
  }
23
24
  const { value, date } = match;
24
- const cacheControl = value.headers.get('cache-control');
25
- const maxAge = parseInt(cacheControl.match(/max-age=(\d+)/)[1], 10);
26
- const swr = parseInt(cacheControl.match(/stale-while-revalidate=(\d+)/)[1], 10);
27
- const age = (new Date().valueOf() - date) / 1000;
25
+ const cacheControl = value.headers.get('cache-control') || '';
26
+ const maxAge = parseInt(((_a = cacheControl.match(/max-age=(\d+)/)) === null || _a === void 0 ? void 0 : _a[1]) || '0', 10);
27
+ const swr = parseInt(((_b = cacheControl.match(/stale-while-revalidate=(\d+)/)) === null || _b === void 0 ? void 0 : _b[1]) || '0', 10);
28
+ const age = (new Date().valueOf() - date.valueOf()) / 1000;
28
29
  const isMiss = age > maxAge + swr;
29
30
  if (isMiss) {
30
31
  logCacheApiStatus('MISS', request.url);
@@ -34,7 +35,7 @@ export class InMemoryCache {
34
35
  const isStale = age > maxAge;
35
36
  const headers = new Headers(value.headers);
36
37
  headers.set('cache', isStale ? 'STALE' : 'HIT');
37
- headers.set('date', date.toGMTString());
38
+ headers.set('date', date.toUTCString());
38
39
  logCacheApiStatus(headers.get('cache'), request.url);
39
40
  const response = new Response(value.body, {
40
41
  headers,
@@ -45,4 +46,13 @@ export class InMemoryCache {
45
46
  this.store.delete(request.url);
46
47
  logCacheApiStatus('DELETE', request.url);
47
48
  }
49
+ keys(request) {
50
+ const cacheKeys = [];
51
+ for (const url of this.store.keys()) {
52
+ if (!request || request.url === url) {
53
+ cacheKeys.push(new Request(url));
54
+ }
55
+ }
56
+ return Promise.resolve(cacheKeys);
57
+ }
48
58
  }
@@ -6,22 +6,25 @@ import { getConfig } from '../../framework/config';
6
6
  import { useServerRequest } from '../../foundation/ServerRequestProvider';
7
7
  import { injectGraphQLTracker } from '../../utilities/graphql-tracker';
8
8
  import { sendMessageToClient } from '../../utilities/devtools';
9
+ import { META_ENV_SSR } from '../../foundation/ssr-interop';
10
+ // Check if the response body has GraphQL errors
11
+ const shouldCacheResponse = (body) => { var _a; return !((body === null || body === void 0 ? void 0 : body.error) || ((_a = body === null || body === void 0 ? void 0 : body.data) === null || _a === void 0 ? void 0 : _a.errors)); };
9
12
  /**
10
13
  * The `useShopQuery` hook allows you to make server-only GraphQL queries to the Storefront API. It must be a descendent of a `ShopifyProvider` component.
11
14
  */
12
15
  export function useShopQuery({ query, variables = {}, cache, locale = '', preload = false, }) {
13
16
  var _a;
14
- if (!import.meta.env.SSR) {
17
+ if (!META_ENV_SSR) {
15
18
  throw new Error('Shopify Storefront API requests should only be made from the server.');
16
19
  }
17
20
  const serverRequest = useServerRequest();
18
21
  const log = getLoggerWithContext(serverRequest);
19
22
  const body = query ? graphqlRequestBody(query, variables) : '';
20
- const { key, url, requestInit } = createShopRequest(body, locale);
23
+ const { key, url, requestInit } = useCreateShopRequest(body, locale);
21
24
  const { data, error: useQueryError } = useQuery(key, query
22
25
  ? fetchBuilder(url, requestInit)
23
26
  : // If no query, avoid calling SFAPI & return nothing
24
- async () => ({ data: undefined, errors: undefined }), { cache, preload });
27
+ async () => ({ data: undefined, errors: undefined }), { cache, shouldCacheResponse, preload });
25
28
  /**
26
29
  * The fetch request itself failed, so we handle that differently than a GraphQL error
27
30
  */
@@ -42,7 +45,7 @@ export function useShopQuery({ query, variables = {}, cache, locale = '', preloa
42
45
  * get returned to the consumer.
43
46
  */
44
47
  if (data === null || data === void 0 ? void 0 : data.errors) {
45
- const errors = data.errors instanceof Array ? data.errors : [data.errors];
48
+ const errors = Array.isArray(data.errors) ? data.errors : [data.errors];
46
49
  for (const error of errors) {
47
50
  if (getConfig().dev) {
48
51
  throw new Error(error.message);
@@ -53,7 +56,7 @@ export function useShopQuery({ query, variables = {}, cache, locale = '', preloa
53
56
  }
54
57
  log.error(`GraphQL errors: ${errors.length}`);
55
58
  }
56
- if (import.meta.env.DEV &&
59
+ if (__DEV__ &&
57
60
  log.options().showUnusedQueryProperties &&
58
61
  query &&
59
62
  typeof query !== 'string' &&
@@ -90,9 +93,14 @@ export function useShopQuery({ query, variables = {}, cache, locale = '', preloa
90
93
  }
91
94
  return data;
92
95
  }
93
- function createShopRequest(body, locale) {
94
- var _a;
96
+ function useCreateShopRequest(body, locale) {
97
+ var _a, _b;
95
98
  const { storeDomain, storefrontToken, storefrontApiVersion, locale: defaultLocale, } = useShop();
99
+ const request = useServerRequest();
100
+ const secretToken = typeof Oxygen !== 'undefined'
101
+ ? (_a = Oxygen === null || Oxygen === void 0 ? void 0 : Oxygen.env) === null || _a === void 0 ? void 0 : _a.SHOPIFY_STOREFRONT_API_SECRET_TOKEN
102
+ : null;
103
+ const buyerIp = request.getBuyerIp();
96
104
  return {
97
105
  key: [storeDomain, storefrontApiVersion, body, locale],
98
106
  url: `https://${storeDomain}/api/${storefrontApiVersion}/graphql.json`,
@@ -100,18 +108,19 @@ function createShopRequest(body, locale) {
100
108
  body,
101
109
  method: 'POST',
102
110
  headers: {
103
- 'X-Shopify-Storefront-Access-Token': storefrontToken,
111
+ 'X-Shopify-Storefront-Access-Token': secretToken !== null && secretToken !== void 0 ? secretToken : storefrontToken,
112
+ 'Shopify-Storefront-Buyer-IP': buyerIp !== null && buyerIp !== void 0 ? buyerIp : '',
104
113
  'X-SDK-Variant': 'hydrogen',
105
114
  'X-SDK-Version': storefrontApiVersion,
106
115
  'content-type': 'application/json',
107
- 'Accept-Language': (_a = locale) !== null && _a !== void 0 ? _a : defaultLocale,
116
+ 'Accept-Language': (_b = locale) !== null && _b !== void 0 ? _b : defaultLocale,
108
117
  },
109
118
  },
110
119
  };
111
120
  }
112
121
  function createErrorMessage(fetchError) {
113
122
  if (fetchError instanceof Response) {
114
- `An error occurred while fetching from the Storefront API. ${
123
+ return `An error occurred while fetching from the Storefront API. ${
115
124
  // 403s to the SF API (almost?) always mean that your Shopify credentials are bad/wrong
116
125
  fetchError.status === 403
117
126
  ? `You may have a bad value in 'shopify.config.js'`
@@ -1,10 +1,9 @@
1
1
  import '../utilities/web-api-polyfill';
2
2
  import connect from 'connect';
3
3
  declare type CreateServerOptions = {
4
- port?: number | string;
4
+ cache?: Cache;
5
5
  };
6
- export declare function createServer({ port, }?: CreateServerOptions): Promise<{
6
+ export declare function createServer({ cache }?: CreateServerOptions): Promise<{
7
7
  app: connect.Server;
8
- port: string | number;
9
8
  }>;
10
9
  export {};
@@ -14,7 +14,7 @@ import compression from 'compression';
14
14
  import bodyParser from 'body-parser';
15
15
  import connect from 'connect';
16
16
  const handleRequest = entrypoint;
17
- export async function createServer({ port = process.env.PORT || 8080, } = {}) {
17
+ export async function createServer({ cache } = {}) {
18
18
  // @ts-ignore
19
19
  globalThis.Oxygen = { env: process.env };
20
20
  const app = connect();
@@ -26,11 +26,13 @@ export async function createServer({ port = process.env.PORT || 8080, } = {}) {
26
26
  app.use(hydrogenMiddleware({
27
27
  getServerEntrypoint: () => handleRequest,
28
28
  indexTemplate,
29
+ cache,
29
30
  }));
30
- return { app, port };
31
+ return { app };
31
32
  }
32
33
  if (require.main === module) {
33
- createServer().then(({ app, port }) => {
34
+ createServer().then(({ app }) => {
35
+ const port = process.env.PORT || 8080;
34
36
  app.listen(port, () => {
35
37
  console.log(`Hydrogen server running at http://localhost:${port}`);
36
38
  });
@@ -13,8 +13,9 @@ export default {
13
13
  try {
14
14
  return (await handleRequest(request, {
15
15
  indexTemplate,
16
- cache: caches.default,
16
+ cache: await caches.open('oxygen'),
17
17
  context,
18
+ buyerIpHeader: 'oxygen-buyer-ip',
18
19
  }));
19
20
  }
20
21
  catch (error) {
@@ -10,7 +10,7 @@ export function getApiRoutes(pages, topLevelPath = '*') {
10
10
  const routes = Object.keys(pages)
11
11
  .filter((key) => pages[key].api)
12
12
  .map((key) => {
13
- const path = key
13
+ let path = key
14
14
  .replace('./routes', '')
15
15
  .replace(/\.server\.(t|j)sx?$/, '')
16
16
  /**
@@ -26,6 +26,8 @@ export function getApiRoutes(pages, topLevelPath = '*') {
26
26
  * Convert /[handle].jsx and /[...handle].jsx to /:handle.jsx for react-router-dom
27
27
  */
28
28
  .replace(/\[(?:[.]{3})?(\w+?)\]/g, (_match, param) => `:${param}`);
29
+ if (path.endsWith('/') && path !== '/')
30
+ path = path.substring(0, path.length - 1);
29
31
  /**
30
32
  * Catch-all routes [...handle].jsx don't need an exact match
31
33
  * https://reactrouter.com/core/api/Route/exact-bool
@@ -2,12 +2,9 @@
2
2
  * The `flattenConnection` utility transforms a connection object from the Storefront API (for example, [Product-related connections](/api/storefront/reference/products/product)) into a flat array of nodes.
3
3
  */
4
4
  export function flattenConnection(connection) {
5
- if (!connection.edges || connection.edges.length < 1) {
6
- throw new Error('must have edges');
7
- }
8
- return connection.edges.map((edge) => {
5
+ return (connection.edges || []).map((edge) => {
9
6
  if (!(edge === null || edge === void 0 ? void 0 : edge.node)) {
10
- throw new Error('must have node');
7
+ throw new Error('Connection edges must contain nodes');
11
8
  }
12
9
  return edge.node;
13
10
  });
@@ -1 +1 @@
1
- export declare const LIB_VERSION = "0.13.0";
1
+ export declare const LIB_VERSION = "0.13.1";
@@ -1 +1 @@
1
- export const LIB_VERSION = '0.13.0';
1
+ export const LIB_VERSION = '0.13.1';
@@ -14,6 +14,7 @@ interface RequestHandlerOptions {
14
14
  dev?: boolean;
15
15
  context?: RuntimeContext;
16
16
  nonce?: string;
17
+ buyerIpHeader?: string;
17
18
  }
18
19
  export interface RequestHandler {
19
20
  (request: Request | IncomingMessage, options: RequestHandlerOptions): Promise<Response | undefined>;
@@ -48,8 +48,9 @@ const DOCTYPE = '<!DOCTYPE html>';
48
48
  const CONTENT_TYPE = 'Content-Type';
49
49
  const HTML_CONTENT_TYPE = 'text/html; charset=UTF-8';
50
50
  const renderHydrogen = (App, { shopifyConfig, routes }) => {
51
- const handleRequest = async function (rawRequest, { indexTemplate, streamableResponse, dev, cache, context, nonce }) {
51
+ const handleRequest = async function (rawRequest, { indexTemplate, streamableResponse, dev, cache, context, nonce, buyerIpHeader, }) {
52
52
  const request = new ServerComponentRequest_server_1.ServerComponentRequest(rawRequest);
53
+ request.ctx.buyerIpHeader = buyerIpHeader;
53
54
  const url = new URL(request.url);
54
55
  const log = (0, log_1.getLoggerWithContext)(request);
55
56
  const componentResponse = new ServerComponentResponse_server_1.ServerComponentResponse();
@@ -57,7 +57,7 @@ function useServerRequest() {
57
57
  const cache = react_1.default.unstable_getCacheForType(requestCacheRSC);
58
58
  request = cache ? cache.get(requestCacheRSC.key) : null;
59
59
  }
60
- catch (error) {
60
+ catch (_a) {
61
61
  // If RSC cache failed it means this is not an RSC request.
62
62
  // Try getting SSR context instead:
63
63
  request = (0, react_1.useContext)(RequestContextSSR);
@@ -35,6 +35,7 @@ export declare class ServerComponentRequest extends Request {
35
35
  queryTimings: Array<QueryTiming>;
36
36
  preloadQueries: PreloadQueriesByURL;
37
37
  router: RouterContextData;
38
+ buyerIpHeader?: string;
38
39
  [key: string]: any;
39
40
  };
40
41
  constructor(input: any);
@@ -44,4 +45,10 @@ export declare class ServerComponentRequest extends Request {
44
45
  savePreloadQuery(query: PreloadQueryEntry): void;
45
46
  getPreloadQueries(): PreloadQueriesByURL | undefined;
46
47
  savePreloadQueries(): void;
48
+ /**
49
+ * Buyer IP varies by hosting provider and runtime. The developer should provide this
50
+ * as an argument to the `handleRequest` function for their runtime.
51
+ * Defaults to `x-forwarded-for` header value.
52
+ */
53
+ getBuyerIp(): string | null;
47
54
  }
@@ -28,13 +28,7 @@ class ServerComponentRequest extends Request {
28
28
  super(input, init);
29
29
  }
30
30
  else {
31
- super(getUrlFromNodeRequest(input), {
32
- headers: new Headers(input.headers),
33
- method: input.method,
34
- body: input.method !== 'GET' && input.method !== 'HEAD'
35
- ? input.body
36
- : undefined,
37
- });
31
+ super(getUrlFromNodeRequest(input), getInitFromNodeRequest(input));
38
32
  }
39
33
  this.time = (0, timing_1.getTime)();
40
34
  this.id = generateId();
@@ -88,6 +82,15 @@ class ServerComponentRequest extends Request {
88
82
  savePreloadQueries() {
89
83
  preloadCache.set(this.preloadURL, this.ctx.preloadQueries);
90
84
  }
85
+ /**
86
+ * Buyer IP varies by hosting provider and runtime. The developer should provide this
87
+ * as an argument to the `handleRequest` function for their runtime.
88
+ * Defaults to `x-forwarded-for` header value.
89
+ */
90
+ getBuyerIp() {
91
+ var _a;
92
+ return this.headers.get((_a = this.ctx.buyerIpHeader) !== null && _a !== void 0 ? _a : 'x-forwarded-for');
93
+ }
91
94
  }
92
95
  exports.ServerComponentRequest = ServerComponentRequest;
93
96
  function mergeMapEntries(map1, map2) {
@@ -116,3 +119,16 @@ function getUrlFromNodeRequest(request) {
116
119
  const secure = request.headers['x-forwarded-proto'] === 'https';
117
120
  return new URL(`${secure ? 'https' : 'http'}://${request.headers.host + url}`).toString();
118
121
  }
122
+ function getInitFromNodeRequest(request) {
123
+ const init = {
124
+ headers: new Headers(request.headers),
125
+ method: request.method,
126
+ body: request.method !== 'GET' && request.method !== 'HEAD'
127
+ ? request.body
128
+ : undefined,
129
+ };
130
+ if (!init.headers.has('x-forwarded-for')) {
131
+ init.headers.set('x-forwarded-for', request.socket.remoteAddress);
132
+ }
133
+ return init;
134
+ }
@@ -8,4 +8,5 @@ export declare class InMemoryCache {
8
8
  put(request: Request, response: Response): void;
9
9
  match(request: Request): Response | undefined;
10
10
  delete(request: Request): void;
11
+ keys(request?: Request): Promise<Request[]>;
11
12
  }
@@ -18,16 +18,17 @@ class InMemoryCache {
18
18
  });
19
19
  }
20
20
  match(request) {
21
+ var _a, _b;
21
22
  const match = this.store.get(request.url);
22
23
  if (!match) {
23
24
  (0, log_1.logCacheApiStatus)('MISS', request.url);
24
25
  return;
25
26
  }
26
27
  const { value, date } = match;
27
- const cacheControl = value.headers.get('cache-control');
28
- const maxAge = parseInt(cacheControl.match(/max-age=(\d+)/)[1], 10);
29
- const swr = parseInt(cacheControl.match(/stale-while-revalidate=(\d+)/)[1], 10);
30
- const age = (new Date().valueOf() - date) / 1000;
28
+ const cacheControl = value.headers.get('cache-control') || '';
29
+ const maxAge = parseInt(((_a = cacheControl.match(/max-age=(\d+)/)) === null || _a === void 0 ? void 0 : _a[1]) || '0', 10);
30
+ const swr = parseInt(((_b = cacheControl.match(/stale-while-revalidate=(\d+)/)) === null || _b === void 0 ? void 0 : _b[1]) || '0', 10);
31
+ const age = (new Date().valueOf() - date.valueOf()) / 1000;
31
32
  const isMiss = age > maxAge + swr;
32
33
  if (isMiss) {
33
34
  (0, log_1.logCacheApiStatus)('MISS', request.url);
@@ -37,7 +38,7 @@ class InMemoryCache {
37
38
  const isStale = age > maxAge;
38
39
  const headers = new Headers(value.headers);
39
40
  headers.set('cache', isStale ? 'STALE' : 'HIT');
40
- headers.set('date', date.toGMTString());
41
+ headers.set('date', date.toUTCString());
41
42
  (0, log_1.logCacheApiStatus)(headers.get('cache'), request.url);
42
43
  const response = new Response(value.body, {
43
44
  headers,
@@ -48,5 +49,14 @@ class InMemoryCache {
48
49
  this.store.delete(request.url);
49
50
  (0, log_1.logCacheApiStatus)('DELETE', request.url);
50
51
  }
52
+ keys(request) {
53
+ const cacheKeys = [];
54
+ for (const url of this.store.keys()) {
55
+ if (!request || request.url === url) {
56
+ cacheKeys.push(new Request(url));
57
+ }
58
+ }
59
+ return Promise.resolve(cacheKeys);
60
+ }
51
61
  }
52
62
  exports.InMemoryCache = InMemoryCache;
@@ -13,7 +13,7 @@ function getApiRoutes(pages, topLevelPath = '*') {
13
13
  const routes = Object.keys(pages)
14
14
  .filter((key) => pages[key].api)
15
15
  .map((key) => {
16
- const path = key
16
+ let path = key
17
17
  .replace('./routes', '')
18
18
  .replace(/\.server\.(t|j)sx?$/, '')
19
19
  /**
@@ -29,6 +29,8 @@ function getApiRoutes(pages, topLevelPath = '*') {
29
29
  * Convert /[handle].jsx and /[...handle].jsx to /:handle.jsx for react-router-dom
30
30
  */
31
31
  .replace(/\[(?:[.]{3})?(\w+?)\]/g, (_match, param) => `:${param}`);
32
+ if (path.endsWith('/') && path !== '/')
33
+ path = path.substring(0, path.length - 1);
32
34
  /**
33
35
  * Catch-all routes [...handle].jsx don't need an exact match
34
36
  * https://reactrouter.com/core/api/Route/exact-bool
@@ -1 +1 @@
1
- export declare const LIB_VERSION = "0.13.0";
1
+ export declare const LIB_VERSION = "0.13.1";
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LIB_VERSION = void 0;
4
- exports.LIB_VERSION = '0.13.0';
4
+ exports.LIB_VERSION = '0.13.1';
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "engines": {
8
8
  "node": ">=14"
9
9
  },
10
- "version": "0.13.0",
10
+ "version": "0.13.1",
11
11
  "description": "Modern custom Shopify storefronts",
12
12
  "license": "MIT",
13
13
  "main": "dist/esnext/index.js",