@shopify/hydrogen 0.8.1 → 0.9.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/dist/esnext/components/ProductProvider/ProductProvider.client.d.ts +3 -2
- package/dist/esnext/components/ShopPayButton/ShopPayButton.client.js +1 -1
- package/dist/esnext/entry-server.js +27 -20
- package/dist/esnext/foundation/RenderCacheProvider/hook.js +1 -1
- package/dist/esnext/foundation/RenderCacheProvider/types.d.ts +9 -2
- package/dist/esnext/foundation/Router/DefaultRoutes.d.ts +1 -1
- package/dist/esnext/foundation/Router/DefaultRoutes.js +6 -2
- package/dist/esnext/foundation/useQuery/hooks.js +3 -2
- package/dist/esnext/framework/Hydration/ServerComponentRequest.server.js +3 -0
- package/dist/esnext/framework/middleware.js +8 -18
- package/dist/esnext/framework/plugin.d.ts +1 -1
- package/dist/esnext/framework/plugin.js +3 -1
- package/dist/esnext/framework/plugins/vite-plugin-hydrogen-middleware.js +2 -0
- package/dist/esnext/framework/plugins/vite-plugin-purge-query-cache.d.ts +3 -0
- package/dist/esnext/framework/plugins/vite-plugin-purge-query-cache.js +11 -0
- package/dist/esnext/handle-event.js +12 -8
- package/dist/esnext/hooks/useShopQuery/hooks.d.ts +3 -1
- package/dist/esnext/hooks/useShopQuery/hooks.js +27 -7
- package/dist/esnext/index.d.ts +2 -1
- package/dist/esnext/index.js +2 -1
- package/dist/esnext/types.d.ts +9 -8
- package/dist/esnext/utilities/apiRoutes.d.ts +21 -0
- package/dist/esnext/utilities/apiRoutes.js +73 -0
- package/dist/esnext/utilities/fetch.js +3 -6
- package/dist/esnext/utilities/flattenConnection/flattenConnection.d.ts +1 -1
- package/dist/esnext/utilities/flattenConnection/flattenConnection.js +1 -1
- package/dist/esnext/utilities/index.d.ts +0 -1
- package/dist/esnext/utilities/index.js +0 -1
- package/dist/esnext/utilities/log/log.d.ts +2 -5
- package/dist/esnext/utilities/log/log.js +27 -22
- package/dist/esnext/utilities/matchPath.d.ts +10 -0
- package/dist/esnext/utilities/matchPath.js +54 -0
- package/dist/esnext/version.d.ts +1 -1
- package/dist/esnext/version.js +1 -1
- package/dist/node/framework/Hydration/ServerComponentRequest.server.js +3 -0
- package/dist/node/framework/middleware.js +8 -18
- package/dist/node/framework/plugin.d.ts +1 -1
- package/dist/node/framework/plugin.js +3 -1
- package/dist/node/framework/plugins/vite-plugin-hydrogen-middleware.js +2 -0
- package/dist/node/framework/plugins/vite-plugin-purge-query-cache.d.ts +3 -0
- package/dist/node/framework/plugins/vite-plugin-purge-query-cache.js +16 -0
- package/dist/node/handle-event.js +12 -8
- package/dist/node/types.d.ts +9 -8
- package/dist/node/utilities/apiRoutes.d.ts +21 -0
- package/dist/node/utilities/apiRoutes.js +79 -0
- package/dist/node/utilities/fetch.js +3 -6
- package/dist/node/utilities/flattenConnection/flattenConnection.d.ts +1 -1
- package/dist/node/utilities/flattenConnection/flattenConnection.js +1 -1
- package/dist/node/utilities/index.d.ts +0 -1
- package/dist/node/utilities/index.js +1 -7
- package/dist/node/utilities/log/log.d.ts +2 -5
- package/dist/node/utilities/log/log.js +28 -23
- package/dist/node/utilities/matchPath.d.ts +10 -0
- package/dist/node/utilities/matchPath.js +58 -0
- package/dist/node/version.d.ts +1 -1
- package/dist/node/version.js +1 -1
- package/dist/worker/framework/Hydration/ServerComponentRequest.server.js +3 -0
- package/dist/worker/handle-event.js +12 -8
- package/dist/worker/types.d.ts +9 -8
- package/dist/worker/utilities/apiRoutes.d.ts +21 -0
- package/dist/worker/utilities/apiRoutes.js +73 -0
- package/dist/worker/utilities/log/log.d.ts +2 -5
- package/dist/worker/utilities/log/log.js +27 -22
- package/dist/worker/utilities/matchPath.d.ts +10 -0
- package/dist/worker/utilities/matchPath.js +54 -0
- package/package.json +11 -4
- package/dist/node/utilities/log/index.d.ts +0 -1
- package/dist/node/utilities/log/index.js +0 -9
- package/dist/worker/utilities/log/index.d.ts +0 -1
- package/dist/worker/utilities/log/index.js +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
+
import { useProductOptions } from '../../hooks';
|
|
2
3
|
import { Product } from './types';
|
|
3
4
|
/**
|
|
4
5
|
* The `ProductProvider` component sets up a context with product details. Descendents of
|
|
@@ -9,8 +10,8 @@ export declare function ProductProvider({ children, product, initialVariantId, }
|
|
|
9
10
|
children: ReactNode;
|
|
10
11
|
/** A [Product object](/api/storefront/reference/products/product). */
|
|
11
12
|
product: Product;
|
|
12
|
-
/** The initially selected variant.
|
|
13
|
-
initialVariantId
|
|
13
|
+
/** The initially selected variant. This is required only if you're using a `SelectedVariantX` hook in the `ProductProvider` component.*/
|
|
14
|
+
initialVariantId?: Parameters<typeof useProductOptions>['0']['initialVariantId'];
|
|
14
15
|
}): JSX.Element;
|
|
15
16
|
export declare namespace ProductProvider {
|
|
16
17
|
var Fragment: string;
|
|
@@ -11,7 +11,7 @@ export function ShopPayButton({ variantIds, className }) {
|
|
|
11
11
|
const { storeDomain } = useShop();
|
|
12
12
|
useEffect(() => {
|
|
13
13
|
const ids = variantIds.reduce((accumulator, gid) => {
|
|
14
|
-
const id =
|
|
14
|
+
const id = gid.split('/').pop();
|
|
15
15
|
if (id) {
|
|
16
16
|
accumulator.push(id);
|
|
17
17
|
}
|
|
@@ -5,7 +5,7 @@ renderToPipeableStream, // Only available in Node context
|
|
|
5
5
|
// @ts-ignore
|
|
6
6
|
renderToReadableStream, // Only available in Browser/Worker context
|
|
7
7
|
} from 'react-dom/server';
|
|
8
|
-
import { logServerResponse } from './utilities/log/log';
|
|
8
|
+
import { logServerResponse, getLoggerFromContext, log, } from './utilities/log/log';
|
|
9
9
|
import { renderToString } from 'react-dom/server';
|
|
10
10
|
import { getErrorMarkup } from './utilities/error';
|
|
11
11
|
import ssrPrepass from 'react-ssr-prepass';
|
|
@@ -18,6 +18,7 @@ import { HydrationWriter } from './framework/Hydration/writer.server';
|
|
|
18
18
|
import { ServerComponentResponse } from './framework/Hydration/ServerComponentResponse.server';
|
|
19
19
|
import { getCacheControlHeader } from './framework/cache';
|
|
20
20
|
import { RenderCacheProvider } from './foundation/RenderCacheProvider';
|
|
21
|
+
import { getApiRouteFromURL, getApiRoutesFromPages } from './utilities/apiRoutes';
|
|
21
22
|
/**
|
|
22
23
|
* react-dom/unstable-fizz provides different entrypoints based on runtime:
|
|
23
24
|
* - `renderToReadableStream` for "browser" (aka worker)
|
|
@@ -30,14 +31,15 @@ const isWorker = Boolean(renderToReadableStream);
|
|
|
30
31
|
* on the client to hydrate and build the React tree.
|
|
31
32
|
*/
|
|
32
33
|
const STREAM_ABORT_TIMEOUT_MS = 3000;
|
|
33
|
-
const renderHydrogen = (App, hook) => {
|
|
34
|
+
const renderHydrogen = (App, { pages } = {}, hook) => {
|
|
34
35
|
/**
|
|
35
36
|
* The render function is responsible for turning the provided `App` into an HTML string,
|
|
36
37
|
* and returning any initial state that needs to be hydrated into the client version of the app.
|
|
37
38
|
* NOTE: This is currently only used for SEO bots or Worker runtime (where Stream is not yet supported).
|
|
38
39
|
*/
|
|
39
|
-
const render = async function (url, { context, request, isReactHydrationRequest, dev
|
|
40
|
-
var _a, _b;
|
|
40
|
+
const render = async function (url, { context, request, isReactHydrationRequest, dev }) {
|
|
41
|
+
var _a, _b, _c, _d, _f;
|
|
42
|
+
const log = getLoggerFromContext(request);
|
|
41
43
|
const state = isReactHydrationRequest
|
|
42
44
|
? JSON.parse((_b = (_a = url.searchParams) === null || _a === void 0 ? void 0 : _a.get('state')) !== null && _b !== void 0 ? _b : '{}')
|
|
43
45
|
: { pathname: url.pathname, search: url.search };
|
|
@@ -48,8 +50,10 @@ const renderHydrogen = (App, hook) => {
|
|
|
48
50
|
request,
|
|
49
51
|
dev,
|
|
50
52
|
log,
|
|
53
|
+
pages,
|
|
51
54
|
});
|
|
52
55
|
const body = await renderApp(ReactApp, state, log, isReactHydrationRequest);
|
|
56
|
+
logServerResponse('ssr', log, request, (_f = (_d = (_c = componentResponse.customStatus) === null || _c === void 0 ? void 0 : _c.code) !== null && _d !== void 0 ? _d : componentResponse.status) !== null && _f !== void 0 ? _f : 200);
|
|
53
57
|
if (componentResponse.customBody) {
|
|
54
58
|
return { body: await componentResponse.customBody, url, componentResponse };
|
|
55
59
|
}
|
|
@@ -66,7 +70,8 @@ const renderHydrogen = (App, hook) => {
|
|
|
66
70
|
* Stream a response to the client. NOTE: This omits custom `<head>`
|
|
67
71
|
* information, so this method should not be used by crawlers.
|
|
68
72
|
*/
|
|
69
|
-
const stream = function (url, { context, request, response, template, dev
|
|
73
|
+
const stream = function (url, { context, request, response, template, dev }) {
|
|
74
|
+
const log = getLoggerFromContext(request);
|
|
70
75
|
const state = { pathname: url.pathname, search: url.search };
|
|
71
76
|
const { ReactApp, componentResponse } = buildReactApp({
|
|
72
77
|
App,
|
|
@@ -75,13 +80,14 @@ const renderHydrogen = (App, hook) => {
|
|
|
75
80
|
request,
|
|
76
81
|
dev,
|
|
77
82
|
log,
|
|
83
|
+
pages,
|
|
78
84
|
});
|
|
79
85
|
response.socket.on('error', (error) => {
|
|
80
86
|
log.fatal(error);
|
|
81
87
|
});
|
|
82
88
|
let didError;
|
|
83
89
|
const head = template.match(/<head>(.+?)<\/head>/s)[1];
|
|
84
|
-
const { pipe
|
|
90
|
+
const { pipe } = renderToPipeableStream(React.createElement(Html, { head: head },
|
|
85
91
|
React.createElement(ReactApp, { ...state })), {
|
|
86
92
|
onCompleteShell() {
|
|
87
93
|
/**
|
|
@@ -132,17 +138,14 @@ const renderHydrogen = (App, hook) => {
|
|
|
132
138
|
});
|
|
133
139
|
const streamTimeout = setTimeout(() => {
|
|
134
140
|
const errorMessage = `The app failed to stream after ${STREAM_ABORT_TIMEOUT_MS} ms`;
|
|
135
|
-
log.
|
|
136
|
-
if (dev && response.headersSent) {
|
|
137
|
-
response.write(getErrorMarkup(new Error(errorMessage)));
|
|
138
|
-
}
|
|
139
|
-
abort();
|
|
141
|
+
log.warn(errorMessage);
|
|
140
142
|
}, STREAM_ABORT_TIMEOUT_MS);
|
|
141
143
|
};
|
|
142
144
|
/**
|
|
143
145
|
* Stream a hydration response to the client.
|
|
144
146
|
*/
|
|
145
|
-
const hydrate = function (url, { context, request, response, dev
|
|
147
|
+
const hydrate = function (url, { context, request, response, dev }) {
|
|
148
|
+
const log = getLoggerFromContext(request);
|
|
146
149
|
const state = JSON.parse(url.searchParams.get('state') || '{}');
|
|
147
150
|
const { ReactApp, componentResponse } = buildReactApp({
|
|
148
151
|
App,
|
|
@@ -151,13 +154,14 @@ const renderHydrogen = (App, hook) => {
|
|
|
151
154
|
request,
|
|
152
155
|
dev,
|
|
153
156
|
log,
|
|
157
|
+
pages,
|
|
154
158
|
});
|
|
155
159
|
response.socket.on('error', (error) => {
|
|
156
160
|
log.fatal(error);
|
|
157
161
|
});
|
|
158
162
|
let didError;
|
|
159
163
|
const writer = new HydrationWriter();
|
|
160
|
-
const { pipe
|
|
164
|
+
const { pipe } = renderToPipeableStream(React.createElement(HydrationContext.Provider, { value: true },
|
|
161
165
|
React.createElement(ReactApp, { ...state })), {
|
|
162
166
|
/**
|
|
163
167
|
* When hydrating, we have to wait until `onCompleteAll` to avoid having
|
|
@@ -180,19 +184,22 @@ const renderHydrogen = (App, hook) => {
|
|
|
180
184
|
},
|
|
181
185
|
});
|
|
182
186
|
const renderTimeout = setTimeout(() => {
|
|
183
|
-
|
|
184
|
-
didError = new Error(errorMessage);
|
|
185
|
-
log.error(errorMessage);
|
|
186
|
-
abort();
|
|
187
|
+
log.error(`The app failed to render RSC after ${STREAM_ABORT_TIMEOUT_MS} ms`);
|
|
187
188
|
}, STREAM_ABORT_TIMEOUT_MS);
|
|
188
189
|
};
|
|
190
|
+
function getApiRoute(url) {
|
|
191
|
+
const routes = getApiRoutesFromPages(pages);
|
|
192
|
+
return getApiRouteFromURL(url, routes);
|
|
193
|
+
}
|
|
189
194
|
return {
|
|
190
195
|
render,
|
|
191
196
|
stream,
|
|
192
197
|
hydrate,
|
|
198
|
+
getApiRoute,
|
|
199
|
+
log,
|
|
193
200
|
};
|
|
194
201
|
};
|
|
195
|
-
function buildReactApp({ App, state, context, request, dev, log, }) {
|
|
202
|
+
function buildReactApp({ App, state, context, request, dev, log, pages, }) {
|
|
196
203
|
const renderCache = {};
|
|
197
204
|
const helmetContext = {};
|
|
198
205
|
const componentResponse = new ServerComponentResponse();
|
|
@@ -204,7 +211,7 @@ function buildReactApp({ App, state, context, request, dev, log, }) {
|
|
|
204
211
|
const ReactApp = (props) => (React.createElement(RenderCacheProvider, { cache: renderCache },
|
|
205
212
|
React.createElement(StaticRouter, { location: { pathname: state.pathname, search: state.search }, context: context },
|
|
206
213
|
React.createElement(HelmetProvider, { context: helmetContext },
|
|
207
|
-
React.createElement(App, { ...props, ...hydrogenServerProps })))));
|
|
214
|
+
React.createElement(App, { ...props, ...hydrogenServerProps, pages: pages })))));
|
|
208
215
|
return { helmetContext, ReactApp, componentResponse };
|
|
209
216
|
}
|
|
210
217
|
function extractHeadElements(helmetContext) {
|
|
@@ -244,7 +251,7 @@ async function renderApp(ReactApp, state, log, isReactHydrationRequest) {
|
|
|
244
251
|
function renderAppFromBufferedStream(app, log, isReactHydrationRequest) {
|
|
245
252
|
return new Promise((resolve, reject) => {
|
|
246
253
|
const errorTimeout = setTimeout(() => {
|
|
247
|
-
|
|
254
|
+
log.warn(`The app failed to SSR after ${STREAM_ABORT_TIMEOUT_MS} ms`);
|
|
248
255
|
}, STREAM_ABORT_TIMEOUT_MS);
|
|
249
256
|
if (isWorker) {
|
|
250
257
|
let isComplete = false;
|
|
@@ -25,7 +25,7 @@ export function useRenderCacheData(key, fetcher) {
|
|
|
25
25
|
if (data !== undefined)
|
|
26
26
|
return data;
|
|
27
27
|
if (!promise) {
|
|
28
|
-
promise = fetcher().then((r) => (data = { data: r }), (e) => (data = {
|
|
28
|
+
promise = fetcher().then((r) => (data = { data: r }), (e) => (data = { error: e }));
|
|
29
29
|
}
|
|
30
30
|
throw promise;
|
|
31
31
|
};
|
|
@@ -6,6 +6,13 @@ export declare type RenderCacheProviderProps = {
|
|
|
6
6
|
cache: RenderCache;
|
|
7
7
|
children?: React.ReactNode;
|
|
8
8
|
};
|
|
9
|
-
export
|
|
9
|
+
export declare type RenderCacheResult<T> = RenderCacheResultSuccess<T> | RenderCacheResultError;
|
|
10
|
+
declare type RenderCacheResultSuccess<T> = {
|
|
10
11
|
data: T;
|
|
11
|
-
|
|
12
|
+
error?: never;
|
|
13
|
+
};
|
|
14
|
+
declare type RenderCacheResultError = {
|
|
15
|
+
data?: never;
|
|
16
|
+
error: Response;
|
|
17
|
+
};
|
|
18
|
+
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ReactElement } from 'react';
|
|
2
2
|
import { Logger } from '../../utilities/log/log';
|
|
3
|
-
|
|
3
|
+
import type { ImportGlobEagerOutput } from '../../types';
|
|
4
4
|
/**
|
|
5
5
|
* Build a set of default Hydrogen routes based on the output provided by Vite's
|
|
6
6
|
* import.meta.globEager method.
|
|
@@ -16,7 +16,8 @@ export function DefaultRoutes({ pages, serverState, fallback, log, }) {
|
|
|
16
16
|
}
|
|
17
17
|
export function createRoutesFromPages(pages, topLevelPath = '*') {
|
|
18
18
|
const topLevelPrefix = topLevelPath.replace('*', '').replace(/\/$/, '');
|
|
19
|
-
const routes = Object.keys(pages)
|
|
19
|
+
const routes = Object.keys(pages)
|
|
20
|
+
.map((key) => {
|
|
20
21
|
const path = key
|
|
21
22
|
.replace('./pages', '')
|
|
22
23
|
.replace(/\.server\.(t|j)sx?$/, '')
|
|
@@ -38,12 +39,15 @@ export function createRoutesFromPages(pages, topLevelPath = '*') {
|
|
|
38
39
|
* https://reactrouter.com/core/api/Route/exact-bool
|
|
39
40
|
*/
|
|
40
41
|
const exact = !/\[(?:[.]{3})(\w+?)\]/.test(key);
|
|
42
|
+
if (!pages[key].default && !pages[key].api)
|
|
43
|
+
throw new Error(`${key} doesn't export a default React component or an API function`);
|
|
41
44
|
return {
|
|
42
45
|
path: topLevelPrefix + path,
|
|
43
46
|
component: pages[key].default,
|
|
44
47
|
exact,
|
|
45
48
|
};
|
|
46
|
-
})
|
|
49
|
+
})
|
|
50
|
+
.filter((route) => route.component);
|
|
47
51
|
/**
|
|
48
52
|
* Place static paths BEFORE dynamic paths to grant priority.
|
|
49
53
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { log } from '../../utilities';
|
|
1
|
+
import { log } from '../../utilities/log';
|
|
2
2
|
import { deleteItemFromCache, getItemFromCache, isStale, setItemInCache, } from '../../framework/cache';
|
|
3
3
|
import { runDelayedFunction } from '../../framework/runtime';
|
|
4
4
|
import { useRenderCacheData } from '../RenderCacheProvider/hook';
|
|
@@ -14,7 +14,8 @@ key,
|
|
|
14
14
|
queryFn,
|
|
15
15
|
/** Options including `cache` to manage the cache behavior of the sub-request. */
|
|
16
16
|
queryOptions) {
|
|
17
|
-
|
|
17
|
+
const withCacheIdKey = ['__QUERY_CACHE_ID__', ...key];
|
|
18
|
+
return useRenderCacheData(withCacheIdKey, cachedQueryFnBuilder(withCacheIdKey, queryFn, queryOptions));
|
|
18
19
|
}
|
|
19
20
|
function cachedQueryFnBuilder(key, queryFn, queryOptions) {
|
|
20
21
|
const resolvedQueryOptions = {
|
|
@@ -15,6 +15,9 @@ export class ServerComponentRequest extends Request {
|
|
|
15
15
|
super(getUrlFromNodeRequest(input), {
|
|
16
16
|
headers: new Headers(input.headers),
|
|
17
17
|
method: input.method,
|
|
18
|
+
body: input.method !== 'GET' && input.method !== 'HEAD'
|
|
19
|
+
? input.body
|
|
20
|
+
: undefined,
|
|
18
21
|
});
|
|
19
22
|
}
|
|
20
23
|
this.time = getTime();
|
|
@@ -16,17 +16,6 @@ export function graphiqlMiddleware({ shopifyConfig, dev, }) {
|
|
|
16
16
|
export function hydrogenMiddleware({ dev, cache, indexTemplate, getServerEntrypoint, devServer, }) {
|
|
17
17
|
return async function (request, response, next) {
|
|
18
18
|
const url = new URL('http://' + request.headers.host + request.originalUrl);
|
|
19
|
-
const isReactHydrationRequest = url.pathname === '/react';
|
|
20
|
-
/**
|
|
21
|
-
* If it's a dev environment, it's assumed that Vite's dev server is handling
|
|
22
|
-
* any static or JS requests, so we need to ensure that we don't try to handle them.
|
|
23
|
-
*
|
|
24
|
-
* If it's a product environment, it's assumed that the developer is handling
|
|
25
|
-
* static requests with e.g. static middleware.
|
|
26
|
-
*/
|
|
27
|
-
if (dev && !shouldInterceptRequest(request, isReactHydrationRequest)) {
|
|
28
|
-
return next();
|
|
29
|
-
}
|
|
30
19
|
try {
|
|
31
20
|
/**
|
|
32
21
|
* We're running in the Node.js runtime without access to `fetch`,
|
|
@@ -34,14 +23,17 @@ export function hydrogenMiddleware({ dev, cache, indexTemplate, getServerEntrypo
|
|
|
34
23
|
*/
|
|
35
24
|
if (!globalThis.fetch) {
|
|
36
25
|
const fetch = await import('node-fetch');
|
|
26
|
+
const { default: AbortController } = await import('abort-controller');
|
|
37
27
|
// @ts-ignore
|
|
38
|
-
globalThis.fetch = fetch
|
|
28
|
+
globalThis.fetch = fetch;
|
|
39
29
|
// @ts-ignore
|
|
40
30
|
globalThis.Request = fetch.Request;
|
|
41
31
|
// @ts-ignore
|
|
42
32
|
globalThis.Response = fetch.Response;
|
|
43
33
|
// @ts-ignore
|
|
44
34
|
globalThis.Headers = fetch.Headers;
|
|
35
|
+
// @ts-ignore
|
|
36
|
+
globalThis.AbortController = AbortController;
|
|
45
37
|
}
|
|
46
38
|
/**
|
|
47
39
|
* Dynamically import ServerComponentResponse after the `fetch`
|
|
@@ -69,7 +61,10 @@ export function hydrogenMiddleware({ dev, cache, indexTemplate, getServerEntrypo
|
|
|
69
61
|
response.setHeader(key, value);
|
|
70
62
|
});
|
|
71
63
|
response.statusCode = eventResponse.status;
|
|
72
|
-
|
|
64
|
+
if (eventResponse.body) {
|
|
65
|
+
response.write(eventResponse.body);
|
|
66
|
+
}
|
|
67
|
+
response.end();
|
|
73
68
|
}
|
|
74
69
|
}
|
|
75
70
|
catch (e) {
|
|
@@ -99,11 +94,6 @@ export function hydrogenMiddleware({ dev, cache, indexTemplate, getServerEntrypo
|
|
|
99
94
|
}
|
|
100
95
|
};
|
|
101
96
|
}
|
|
102
|
-
function shouldInterceptRequest(request, isReactHydrationRequest) {
|
|
103
|
-
var _a;
|
|
104
|
-
return (/text\/html|application\/hydrogen/.test((_a = request.headers['accept']) !== null && _a !== void 0 ? _a : '') ||
|
|
105
|
-
isReactHydrationRequest);
|
|
106
|
-
}
|
|
107
97
|
/**
|
|
108
98
|
* /graphiql and /___graphql are supported
|
|
109
99
|
*/
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { HydrogenVitePluginOptions, ShopifyConfig } from '../types';
|
|
2
|
-
declare const _default: (shopifyConfig: ShopifyConfig, pluginOptions
|
|
2
|
+
declare const _default: (shopifyConfig: ShopifyConfig, pluginOptions?: HydrogenVitePluginOptions) => (false | "" | import("vite").Plugin | import("vite").PluginOption[] | undefined)[];
|
|
3
3
|
export default _default;
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import hydrogenConfig from './plugins/vite-plugin-hydrogen-config';
|
|
2
2
|
import hydrogenMiddleware from './plugins/vite-plugin-hydrogen-middleware';
|
|
3
3
|
import reactServerComponentShim from './plugins/vite-plugin-react-server-components-shim';
|
|
4
|
+
import purgeQueryCache from './plugins/vite-plugin-purge-query-cache';
|
|
4
5
|
import inspect from 'vite-plugin-inspect';
|
|
5
6
|
import react from '@vitejs/plugin-react';
|
|
6
|
-
export default (shopifyConfig, pluginOptions) => {
|
|
7
|
+
export default (shopifyConfig, pluginOptions = {}) => {
|
|
7
8
|
return [
|
|
8
9
|
process.env.VITE_INSPECT && inspect(),
|
|
9
10
|
hydrogenConfig(),
|
|
10
11
|
hydrogenMiddleware(shopifyConfig, pluginOptions),
|
|
11
12
|
reactServerComponentShim(),
|
|
12
13
|
react(),
|
|
14
|
+
pluginOptions.purgeQueryCacheOnBuild && purgeQueryCache(),
|
|
13
15
|
];
|
|
14
16
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadEnv } from 'vite';
|
|
2
|
+
import bodyParser from 'body-parser';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import { promises as fs } from 'fs';
|
|
4
5
|
import { hydrogenMiddleware, graphiqlMiddleware } from '../middleware';
|
|
@@ -25,6 +26,7 @@ export default (shopifyConfig, pluginOptions) => {
|
|
|
25
26
|
shopifyConfig,
|
|
26
27
|
dev: true,
|
|
27
28
|
}));
|
|
29
|
+
server.middlewares.use(bodyParser.raw({ type: '*/*' }));
|
|
28
30
|
return () => server.middlewares.use(hydrogenMiddleware({
|
|
29
31
|
dev: true,
|
|
30
32
|
shopifyConfig,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import Crypto from 'crypto';
|
|
2
|
+
export default () => {
|
|
3
|
+
const buildCacheId = Crypto.randomBytes(8).toString('hex').slice(0, 8);
|
|
4
|
+
return {
|
|
5
|
+
name: 'vite-plugin-purge-query-cache',
|
|
6
|
+
enforce: 'pre',
|
|
7
|
+
transform(code) {
|
|
8
|
+
return code.replace('__QUERY_CACHE_ID__', buildCacheId);
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getCacheControlHeader } from './framework/cache';
|
|
2
2
|
import { setContext, setCache } from './framework/runtime';
|
|
3
3
|
import { setConfig } from './framework/config';
|
|
4
|
-
import {
|
|
4
|
+
import { renderApiRoute } from './utilities/apiRoutes';
|
|
5
5
|
export default async function handleEvent(event, { request, entrypoint, indexTemplate, assetHandler, streamableResponse, dev, cache, context, }) {
|
|
6
6
|
var _a, _b, _c, _d, _e;
|
|
7
7
|
const url = new URL(request.url);
|
|
@@ -22,15 +22,23 @@ export default async function handleEvent(event, { request, entrypoint, indexTem
|
|
|
22
22
|
assetHandler) {
|
|
23
23
|
return assetHandler(event, url);
|
|
24
24
|
}
|
|
25
|
-
const { render, hydrate, stream } = entrypoint.default || entrypoint;
|
|
25
|
+
const { render, hydrate, stream, getApiRoute, log } = entrypoint.default || entrypoint;
|
|
26
26
|
// @ts-ignore
|
|
27
|
-
if (dev && !(render && hydrate && stream)) {
|
|
27
|
+
if (dev && !(render && hydrate && stream && getApiRoute)) {
|
|
28
28
|
throw new Error(`entry-server.jsx could not be loaded. This likely occurred because of a Vite compilation error.\n` +
|
|
29
29
|
`Please check your server logs for more information.`);
|
|
30
30
|
}
|
|
31
|
+
if (!isReactHydrationRequest) {
|
|
32
|
+
const apiRoute = getApiRoute(url);
|
|
33
|
+
// The API Route might have a default export, making it also a server component
|
|
34
|
+
// If it does, only render the API route if the request method is GET
|
|
35
|
+
if (apiRoute &&
|
|
36
|
+
(!apiRoute.hasServerComponent || request.method !== 'GET')) {
|
|
37
|
+
return renderApiRoute(request, apiRoute, log);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
31
40
|
const userAgent = request.headers.get('user-agent');
|
|
32
41
|
const isStreamable = streamableResponse && !isBotUA(url, userAgent);
|
|
33
|
-
const logger = getLoggerFromContext(request);
|
|
34
42
|
/**
|
|
35
43
|
* Stream back real-user responses, but for bots/etc,
|
|
36
44
|
* use `render` instead. This is because we need to inject <head>
|
|
@@ -43,7 +51,6 @@ export default async function handleEvent(event, { request, entrypoint, indexTem
|
|
|
43
51
|
request,
|
|
44
52
|
response: streamableResponse,
|
|
45
53
|
dev,
|
|
46
|
-
log: logger,
|
|
47
54
|
});
|
|
48
55
|
}
|
|
49
56
|
else {
|
|
@@ -53,7 +60,6 @@ export default async function handleEvent(event, { request, entrypoint, indexTem
|
|
|
53
60
|
response: streamableResponse,
|
|
54
61
|
template,
|
|
55
62
|
dev,
|
|
56
|
-
log: logger,
|
|
57
63
|
});
|
|
58
64
|
}
|
|
59
65
|
return;
|
|
@@ -63,7 +69,6 @@ export default async function handleEvent(event, { request, entrypoint, indexTem
|
|
|
63
69
|
context: {},
|
|
64
70
|
isReactHydrationRequest,
|
|
65
71
|
dev,
|
|
66
|
-
log: logger,
|
|
67
72
|
});
|
|
68
73
|
const headers = componentResponse.headers;
|
|
69
74
|
/**
|
|
@@ -100,7 +105,6 @@ export default async function handleEvent(event, { request, entrypoint, indexTem
|
|
|
100
105
|
headers,
|
|
101
106
|
});
|
|
102
107
|
}
|
|
103
|
-
logServerResponse('ssr', logger, request, response.status);
|
|
104
108
|
return response;
|
|
105
109
|
}
|
|
106
110
|
/**
|
|
@@ -8,7 +8,7 @@ export interface UseShopQueryResponse<T> {
|
|
|
8
8
|
/**
|
|
9
9
|
* The `useShopQuery` hook allows you to make server-only GraphQL queries to the Storefront API. It must be a descendent of a `ShopifyProvider` component.
|
|
10
10
|
*/
|
|
11
|
-
export declare function useShopQuery<T>({ query, variables, cache, }: {
|
|
11
|
+
export declare function useShopQuery<T>({ query, variables, cache, locale, }: {
|
|
12
12
|
/** A string of the GraphQL query.
|
|
13
13
|
* If no query is provided, useShopQuery will make no calls to the Storefront API.
|
|
14
14
|
*/
|
|
@@ -17,4 +17,6 @@ export declare function useShopQuery<T>({ query, variables, cache, }: {
|
|
|
17
17
|
variables?: Record<string, any>;
|
|
18
18
|
/** An object containing cache-control options for the sub-request. */
|
|
19
19
|
cache?: CacheOptions;
|
|
20
|
+
/** A string corresponding to a valid locale identifier like `en-us` used to make the request. */
|
|
21
|
+
locale?: string;
|
|
20
22
|
}): UseShopQueryResponse<T>;
|
|
@@ -1,21 +1,39 @@
|
|
|
1
1
|
import { useShop } from '../../foundation/useShop';
|
|
2
|
-
import { log } from '../../utilities';
|
|
2
|
+
import { log } from '../../utilities/log';
|
|
3
3
|
import { useQuery } from '../../foundation/useQuery';
|
|
4
4
|
import { isClient, fetchBuilder, graphqlRequestBody } from '../../utilities';
|
|
5
5
|
import { getConfig } from '../../framework/config';
|
|
6
6
|
/**
|
|
7
7
|
* The `useShopQuery` hook allows you to make server-only GraphQL queries to the Storefront API. It must be a descendent of a `ShopifyProvider` component.
|
|
8
8
|
*/
|
|
9
|
-
export function useShopQuery({ query, variables = {}, cache = {}, }) {
|
|
9
|
+
export function useShopQuery({ query, variables = {}, cache = {}, locale = '', }) {
|
|
10
10
|
if (isClient()) {
|
|
11
11
|
throw new Error('Shopify Storefront API requests should only be made from the server.');
|
|
12
12
|
}
|
|
13
13
|
const body = query ? graphqlRequestBody(query, variables) : '';
|
|
14
|
-
const { request, key } = createShopRequest(body);
|
|
15
|
-
const { data } = useQuery(key, query
|
|
14
|
+
const { request, key } = createShopRequest(body, locale);
|
|
15
|
+
const { data, error: fetchError } = useQuery(key, query
|
|
16
16
|
? fetchBuilder(request)
|
|
17
17
|
: // If no query, avoid calling SFAPI & return nothing
|
|
18
18
|
async () => ({ data: undefined, errors: undefined }), { cache });
|
|
19
|
+
/**
|
|
20
|
+
* The fetch request itself failed, so we handle that differently than a GraphQL error
|
|
21
|
+
*/
|
|
22
|
+
if (fetchError) {
|
|
23
|
+
const errorMessage = `Failed to fetch the Storefront API. ${
|
|
24
|
+
// 403s to the SF API (almost?) always mean that your Shopify credentials are bad/wrong
|
|
25
|
+
fetchError.status === 403
|
|
26
|
+
? `You may have a bad value in 'shopify.config.js'`
|
|
27
|
+
: `${fetchError.statusText}`}`;
|
|
28
|
+
log.error(errorMessage);
|
|
29
|
+
if (getConfig().dev) {
|
|
30
|
+
throw new Error(errorMessage);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
// in non-dev environments, we probably don't want super-detailed error messages for the user
|
|
34
|
+
throw new Error(`The fetch attempt failed; there was an issue connecting to the data source.`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
19
37
|
/**
|
|
20
38
|
* GraphQL errors get printed to the console but ultimately
|
|
21
39
|
* get returned to the consumer.
|
|
@@ -34,8 +52,9 @@ export function useShopQuery({ query, variables = {}, cache = {}, }) {
|
|
|
34
52
|
}
|
|
35
53
|
return data;
|
|
36
54
|
}
|
|
37
|
-
function createShopRequest(body) {
|
|
38
|
-
|
|
55
|
+
function createShopRequest(body, locale) {
|
|
56
|
+
var _a;
|
|
57
|
+
const { storeDomain, storefrontToken, graphqlApiVersion, locale: defaultLocale, } = useShop();
|
|
39
58
|
const url = `https://${storeDomain}/api/${graphqlApiVersion}/graphql.json`;
|
|
40
59
|
return {
|
|
41
60
|
request: new Request(url, {
|
|
@@ -43,9 +62,10 @@ function createShopRequest(body) {
|
|
|
43
62
|
headers: {
|
|
44
63
|
'X-Shopify-Storefront-Access-Token': storefrontToken,
|
|
45
64
|
'content-type': 'application/json',
|
|
65
|
+
'Accept-Language': (_a = locale) !== null && _a !== void 0 ? _a : defaultLocale,
|
|
46
66
|
},
|
|
47
67
|
body,
|
|
48
68
|
}),
|
|
49
|
-
key: [storeDomain, graphqlApiVersion, body],
|
|
69
|
+
key: [storeDomain, graphqlApiVersion, body, locale],
|
|
50
70
|
};
|
|
51
71
|
}
|
package/dist/esnext/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export * from './foundation/';
|
|
2
2
|
export * from './components/';
|
|
3
3
|
export * from './hooks/';
|
|
4
|
-
export { flattenConnection, fetchBuilder, graphqlRequestBody, decodeShopifyId, isClient,
|
|
4
|
+
export { flattenConnection, fetchBuilder, graphqlRequestBody, decodeShopifyId, isClient, getTime, } from './utilities';
|
|
5
|
+
export { log, setLogger, Logger } from './utilities/log';
|
|
5
6
|
export { Helmet } from 'react-helmet-async';
|
|
6
7
|
export { LocalizationProvider } from './components/LocalizationProvider/LocalizationProvider.server';
|
|
7
8
|
export * from './hooks/useShopQuery';
|
package/dist/esnext/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export * from './foundation/';
|
|
2
2
|
export * from './components/';
|
|
3
3
|
export * from './hooks/';
|
|
4
|
-
export { flattenConnection, fetchBuilder, graphqlRequestBody, decodeShopifyId, isClient,
|
|
4
|
+
export { flattenConnection, fetchBuilder, graphqlRequestBody, decodeShopifyId, isClient, getTime, } from './utilities';
|
|
5
|
+
export { log, setLogger } from './utilities/log';
|
|
5
6
|
export { Helmet } from 'react-helmet-async';
|
|
6
7
|
// This is exported here because it contains a Server Component
|
|
7
8
|
export { LocalizationProvider } from './components/LocalizationProvider/LocalizationProvider.server';
|
package/dist/esnext/types.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import { ServerResponse } from 'http';
|
|
3
|
-
import type { Logger } from './utilities/log/log';
|
|
4
3
|
import type { ServerComponentResponse } from './framework/Hydration/ServerComponentResponse.server';
|
|
5
4
|
import type { ServerComponentRequest } from './framework/Hydration/ServerComponentRequest.server';
|
|
6
5
|
import type { Metafield, Image, MediaContentType } from './graphql/types/types';
|
|
6
|
+
import { ApiRouteMatch } from './utilities/apiRoutes';
|
|
7
|
+
import { Logger } from './utilities/log/log';
|
|
7
8
|
export declare type Renderer = (url: URL, options: {
|
|
8
9
|
request: ServerComponentRequest;
|
|
9
10
|
context?: Record<string, any>;
|
|
10
11
|
isReactHydrationRequest?: boolean;
|
|
11
12
|
dev?: boolean;
|
|
12
|
-
log: Logger;
|
|
13
13
|
}) => Promise<{
|
|
14
14
|
body: string;
|
|
15
15
|
componentResponse: ServerComponentResponse;
|
|
@@ -19,20 +19,20 @@ export declare type Streamer = (url: URL, options: {
|
|
|
19
19
|
request: ServerComponentRequest;
|
|
20
20
|
response: ServerResponse;
|
|
21
21
|
template: string;
|
|
22
|
-
log: Logger;
|
|
23
22
|
dev?: boolean;
|
|
24
23
|
}) => void;
|
|
25
24
|
export declare type Hydrator = (url: URL, options: {
|
|
26
25
|
context: any;
|
|
27
26
|
request: ServerComponentRequest;
|
|
28
27
|
response: ServerResponse;
|
|
29
|
-
log: Logger;
|
|
30
28
|
dev?: boolean;
|
|
31
29
|
}) => void;
|
|
32
30
|
export declare type EntryServerHandler = {
|
|
33
31
|
render: Renderer;
|
|
34
32
|
stream: Streamer;
|
|
35
33
|
hydrate: Hydrator;
|
|
34
|
+
getApiRoute: (url: URL) => ApiRouteMatch | null;
|
|
35
|
+
log: Logger;
|
|
36
36
|
};
|
|
37
37
|
export declare type ShopifyConfig = {
|
|
38
38
|
locale?: string;
|
|
@@ -43,11 +43,11 @@ export declare type ShopifyConfig = {
|
|
|
43
43
|
export declare type Hook = (params: {
|
|
44
44
|
url: URL;
|
|
45
45
|
} & Record<string, any>) => any | Promise<any>;
|
|
46
|
-
export declare type
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
hydrate: Hydrator;
|
|
46
|
+
export declare type ImportGlobEagerOutput = Record<string, Record<'default' | 'api', any>>;
|
|
47
|
+
export declare type ServerHandlerConfig = {
|
|
48
|
+
pages?: ImportGlobEagerOutput;
|
|
50
49
|
};
|
|
50
|
+
export declare type ServerHandler = (App: any, config?: ServerHandlerConfig, hook?: Hook) => EntryServerHandler;
|
|
51
51
|
export declare type ClientHandler = (App: any, hook?: Hook) => Promise<void>;
|
|
52
52
|
export interface GraphQLConnection<T> {
|
|
53
53
|
edges?: {
|
|
@@ -91,5 +91,6 @@ export interface CacheOptions {
|
|
|
91
91
|
}
|
|
92
92
|
export interface HydrogenVitePluginOptions {
|
|
93
93
|
devCache?: boolean;
|
|
94
|
+
purgeQueryCacheOnBuild?: boolean;
|
|
94
95
|
}
|
|
95
96
|
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ImportGlobEagerOutput } from '../types';
|
|
2
|
+
import { Logger } from '../utilities/log/log';
|
|
3
|
+
declare type RouteParams = Record<string, string>;
|
|
4
|
+
declare type RequestOptions = {
|
|
5
|
+
params: RouteParams;
|
|
6
|
+
};
|
|
7
|
+
declare type ResourceGetter = (request: Request, requestOptions: RequestOptions) => Promise<Response>;
|
|
8
|
+
interface HydrogenApiRoute {
|
|
9
|
+
path: string;
|
|
10
|
+
resource: ResourceGetter;
|
|
11
|
+
hasServerComponent: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare type ApiRouteMatch = {
|
|
14
|
+
resource: ResourceGetter;
|
|
15
|
+
hasServerComponent: boolean;
|
|
16
|
+
params: RouteParams;
|
|
17
|
+
};
|
|
18
|
+
export declare function getApiRoutesFromPages(pages: ImportGlobEagerOutput | undefined, topLevelPath?: string): Array<HydrogenApiRoute>;
|
|
19
|
+
export declare function getApiRouteFromURL(url: URL, routes: Array<HydrogenApiRoute>): ApiRouteMatch | null;
|
|
20
|
+
export declare function renderApiRoute(request: Request, route: ApiRouteMatch, log: Logger): Promise<Response>;
|
|
21
|
+
export {};
|