@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.
- package/CHANGELOG.md +19 -5
- package/dist/esnext/entry-server.d.ts +1 -0
- package/dist/esnext/entry-server.js +2 -1
- package/dist/esnext/foundation/ServerRequestProvider/ServerRequestProvider.js +1 -1
- package/dist/esnext/foundation/useQuery/hooks.d.ts +3 -0
- package/dist/esnext/foundation/useQuery/hooks.js +8 -2
- package/dist/esnext/framework/Hydration/ServerComponentRequest.server.d.ts +7 -0
- package/dist/esnext/framework/Hydration/ServerComponentRequest.server.js +23 -7
- package/dist/esnext/framework/cache/in-memory.d.ts +1 -0
- package/dist/esnext/framework/cache/in-memory.js +15 -5
- package/dist/esnext/hooks/useShopQuery/hooks.js +19 -10
- package/dist/esnext/platforms/node.d.ts +2 -3
- package/dist/esnext/platforms/node.js +5 -3
- package/dist/esnext/platforms/worker.js +2 -1
- package/dist/esnext/utilities/apiRoutes.js +3 -1
- package/dist/esnext/utilities/flattenConnection/flattenConnection.js +2 -5
- package/dist/esnext/version.d.ts +1 -1
- package/dist/esnext/version.js +1 -1
- package/dist/node/entry-server.d.ts +1 -0
- package/dist/node/entry-server.js +2 -1
- package/dist/node/foundation/ServerRequestProvider/ServerRequestProvider.js +1 -1
- package/dist/node/framework/Hydration/ServerComponentRequest.server.d.ts +7 -0
- package/dist/node/framework/Hydration/ServerComponentRequest.server.js +23 -7
- package/dist/node/framework/cache/in-memory.d.ts +1 -0
- package/dist/node/framework/cache/in-memory.js +15 -5
- package/dist/node/utilities/apiRoutes.js +3 -1
- package/dist/node/version.d.ts +1 -1
- package/dist/node/version.js +1 -1
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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.
|
|
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 (!
|
|
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 } =
|
|
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
|
|
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 (
|
|
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
|
|
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': (
|
|
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
|
-
|
|
4
|
+
cache?: Cache;
|
|
5
5
|
};
|
|
6
|
-
export declare function createServer({
|
|
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({
|
|
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
|
|
31
|
+
return { app };
|
|
31
32
|
}
|
|
32
33
|
if (require.main === module) {
|
|
33
|
-
createServer().then(({ app
|
|
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
|
});
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
7
|
+
throw new Error('Connection edges must contain nodes');
|
|
11
8
|
}
|
|
12
9
|
return edge.node;
|
|
13
10
|
});
|
package/dist/esnext/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const LIB_VERSION = "0.13.
|
|
1
|
+
export declare const LIB_VERSION = "0.13.1";
|
package/dist/esnext/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const LIB_VERSION = '0.13.
|
|
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 (
|
|
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
|
+
}
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
package/dist/node/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const LIB_VERSION = "0.13.
|
|
1
|
+
export declare const LIB_VERSION = "0.13.1";
|
package/dist/node/version.js
CHANGED