@shopify/hydrogen 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esnext/components/CartLineQuantityAdjustButton/CartLineQuantityAdjustButton.js +4 -0
- 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 +76 -36
- 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 +4 -2
- package/dist/esnext/foundation/Router/DefaultRoutes.js +8 -4
- package/dist/esnext/foundation/ServerStateProvider/ServerStateProvider.client.js +4 -2
- package/dist/esnext/foundation/useQuery/hooks.js +6 -4
- package/dist/esnext/foundation/useShop/use-shop.d.ts +1 -1
- package/dist/esnext/foundation/useShop/use-shop.js +1 -1
- package/dist/esnext/framework/Hydration/ServerComponentRequest.server.d.ts +1 -0
- package/dist/esnext/framework/Hydration/ServerComponentRequest.server.js +5 -0
- package/dist/esnext/framework/middleware.js +20 -22
- 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-config.d.ts +1 -1
- package/dist/esnext/framework/plugins/vite-plugin-hydrogen-config.js +2 -1
- package/dist/esnext/framework/plugins/vite-plugin-hydrogen-middleware.d.ts +7 -1
- package/dist/esnext/framework/plugins/vite-plugin-hydrogen-middleware.js +17 -1
- 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 +80 -12
- package/dist/esnext/hooks/useShopQuery/hooks.d.ts +4 -4
- package/dist/esnext/hooks/useShopQuery/hooks.js +32 -11
- package/dist/esnext/index.d.ts +2 -1
- package/dist/esnext/index.js +2 -1
- package/dist/esnext/types.d.ts +9 -4
- 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 +1 -0
- package/dist/esnext/utilities/index.js +1 -0
- package/dist/esnext/utilities/log/index.d.ts +1 -0
- package/dist/esnext/utilities/log/index.js +1 -0
- package/dist/esnext/utilities/log/log.d.ts +17 -0
- package/dist/esnext/utilities/log/log.js +76 -0
- package/dist/esnext/utilities/matchPath.d.ts +10 -0
- package/dist/esnext/utilities/matchPath.js +54 -0
- package/dist/esnext/utilities/timing.d.ts +7 -0
- package/dist/esnext/utilities/timing.js +14 -0
- package/dist/esnext/version.d.ts +1 -1
- package/dist/esnext/version.js +1 -1
- package/dist/node/framework/Hydration/ServerComponentRequest.server.d.ts +1 -0
- package/dist/node/framework/Hydration/ServerComponentRequest.server.js +5 -0
- package/dist/node/framework/middleware.js +20 -22
- 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-config.d.ts +1 -1
- package/dist/node/framework/plugins/vite-plugin-hydrogen-config.js +2 -1
- package/dist/node/framework/plugins/vite-plugin-hydrogen-middleware.d.ts +7 -1
- package/dist/node/framework/plugins/vite-plugin-hydrogen-middleware.js +17 -1
- 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 +80 -12
- package/dist/node/types.d.ts +9 -4
- 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 +1 -0
- package/dist/node/utilities/index.js +3 -1
- package/dist/node/utilities/log/log.d.ts +17 -0
- package/dist/node/utilities/log/log.js +83 -0
- package/dist/node/utilities/matchPath.d.ts +10 -0
- package/dist/node/utilities/matchPath.js +58 -0
- package/dist/node/utilities/timing.d.ts +7 -0
- package/dist/node/utilities/timing.js +18 -0
- package/dist/node/version.d.ts +1 -1
- package/dist/node/version.js +1 -1
- package/dist/worker/framework/Hydration/ServerComponentRequest.server.d.ts +1 -0
- package/dist/worker/framework/Hydration/ServerComponentRequest.server.js +5 -0
- package/dist/worker/handle-event.js +80 -12
- package/dist/worker/types.d.ts +9 -4
- 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 +17 -0
- package/dist/worker/utilities/log/log.js +76 -0
- package/dist/worker/utilities/matchPath.d.ts +10 -0
- package/dist/worker/utilities/matchPath.js +54 -0
- package/dist/worker/utilities/timing.d.ts +7 -0
- package/dist/worker/utilities/timing.js +14 -0
- package/package.json +15 -7
|
@@ -17,6 +17,10 @@ export function CartLineQuantityAdjustButton(props) {
|
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
19
|
const quantity = adjust === 'decrease' ? cartLine.quantity - 1 : cartLine.quantity + 1;
|
|
20
|
+
if (quantity <= 0) {
|
|
21
|
+
removeLines([cartLine.id]);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
20
24
|
updateLines([{ id: cartLine.id, quantity }]);
|
|
21
25
|
}, ...passthroughProps }, children));
|
|
22
26
|
}
|
|
@@ -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
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
3
|
// @ts-ignore
|
|
4
|
-
|
|
4
|
+
renderToPipeableStream, // Only available in Node context
|
|
5
|
+
// @ts-ignore
|
|
6
|
+
renderToReadableStream, // Only available in Browser/Worker context
|
|
7
|
+
} from 'react-dom/server';
|
|
8
|
+
import { logServerResponse, getLoggerFromContext, log, } from './utilities/log/log';
|
|
5
9
|
import { renderToString } from 'react-dom/server';
|
|
6
10
|
import { getErrorMarkup } from './utilities/error';
|
|
7
11
|
import ssrPrepass from 'react-ssr-prepass';
|
|
@@ -14,6 +18,7 @@ import { HydrationWriter } from './framework/Hydration/writer.server';
|
|
|
14
18
|
import { ServerComponentResponse } from './framework/Hydration/ServerComponentResponse.server';
|
|
15
19
|
import { getCacheControlHeader } from './framework/cache';
|
|
16
20
|
import { RenderCacheProvider } from './foundation/RenderCacheProvider';
|
|
21
|
+
import { getApiRouteFromURL, getApiRoutesFromPages } from './utilities/apiRoutes';
|
|
17
22
|
/**
|
|
18
23
|
* react-dom/unstable-fizz provides different entrypoints based on runtime:
|
|
19
24
|
* - `renderToReadableStream` for "browser" (aka worker)
|
|
@@ -26,14 +31,15 @@ const isWorker = Boolean(renderToReadableStream);
|
|
|
26
31
|
* on the client to hydrate and build the React tree.
|
|
27
32
|
*/
|
|
28
33
|
const STREAM_ABORT_TIMEOUT_MS = 3000;
|
|
29
|
-
const renderHydrogen = (App, hook) => {
|
|
34
|
+
const renderHydrogen = (App, { pages } = {}, hook) => {
|
|
30
35
|
/**
|
|
31
36
|
* The render function is responsible for turning the provided `App` into an HTML string,
|
|
32
37
|
* and returning any initial state that needs to be hydrated into the client version of the app.
|
|
33
38
|
* NOTE: This is currently only used for SEO bots or Worker runtime (where Stream is not yet supported).
|
|
34
39
|
*/
|
|
35
40
|
const render = async function (url, { context, request, isReactHydrationRequest, dev }) {
|
|
36
|
-
var _a, _b;
|
|
41
|
+
var _a, _b, _c, _d, _f;
|
|
42
|
+
const log = getLoggerFromContext(request);
|
|
37
43
|
const state = isReactHydrationRequest
|
|
38
44
|
? JSON.parse((_b = (_a = url.searchParams) === null || _a === void 0 ? void 0 : _a.get('state')) !== null && _b !== void 0 ? _b : '{}')
|
|
39
45
|
: { pathname: url.pathname, search: url.search };
|
|
@@ -43,8 +49,11 @@ const renderHydrogen = (App, hook) => {
|
|
|
43
49
|
context,
|
|
44
50
|
request,
|
|
45
51
|
dev,
|
|
52
|
+
log,
|
|
53
|
+
pages,
|
|
46
54
|
});
|
|
47
|
-
const body = await renderApp(ReactApp, state, isReactHydrationRequest);
|
|
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);
|
|
48
57
|
if (componentResponse.customBody) {
|
|
49
58
|
return { body: await componentResponse.customBody, url, componentResponse };
|
|
50
59
|
}
|
|
@@ -62,6 +71,7 @@ const renderHydrogen = (App, hook) => {
|
|
|
62
71
|
* information, so this method should not be used by crawlers.
|
|
63
72
|
*/
|
|
64
73
|
const stream = function (url, { context, request, response, template, dev }) {
|
|
74
|
+
const log = getLoggerFromContext(request);
|
|
65
75
|
const state = { pathname: url.pathname, search: url.search };
|
|
66
76
|
const { ReactApp, componentResponse } = buildReactApp({
|
|
67
77
|
App,
|
|
@@ -69,34 +79,37 @@ const renderHydrogen = (App, hook) => {
|
|
|
69
79
|
context,
|
|
70
80
|
request,
|
|
71
81
|
dev,
|
|
82
|
+
log,
|
|
83
|
+
pages,
|
|
72
84
|
});
|
|
73
85
|
response.socket.on('error', (error) => {
|
|
74
|
-
|
|
86
|
+
log.fatal(error);
|
|
75
87
|
});
|
|
76
88
|
let didError;
|
|
77
89
|
const head = template.match(/<head>(.+?)<\/head>/s)[1];
|
|
78
|
-
const {
|
|
79
|
-
React.createElement(ReactApp, { ...state })),
|
|
80
|
-
|
|
90
|
+
const { pipe } = renderToPipeableStream(React.createElement(Html, { head: head },
|
|
91
|
+
React.createElement(ReactApp, { ...state })), {
|
|
92
|
+
onCompleteShell() {
|
|
81
93
|
/**
|
|
82
94
|
* TODO: This assumes `response.cache()` has been called _before_ any
|
|
83
95
|
* queries which might be caught behind Suspense. Clarify this or add
|
|
84
96
|
* additional checks downstream?
|
|
85
97
|
*/
|
|
86
98
|
response.setHeader(getCacheControlHeader({ dev }), componentResponse.cacheControlHeader);
|
|
87
|
-
writeHeadToServerResponse(response, componentResponse, didError);
|
|
99
|
+
writeHeadToServerResponse(request, response, componentResponse, log, didError);
|
|
88
100
|
if (isRedirect(response)) {
|
|
89
101
|
// Return redirects early without further rendering/streaming
|
|
90
102
|
return response.end();
|
|
91
103
|
}
|
|
92
104
|
if (!componentResponse.canStream())
|
|
93
105
|
return;
|
|
94
|
-
startWritingHtmlToServerResponse(response,
|
|
106
|
+
startWritingHtmlToServerResponse(response, pipe, dev ? didError : undefined);
|
|
95
107
|
},
|
|
96
108
|
onCompleteAll() {
|
|
109
|
+
clearTimeout(streamTimeout);
|
|
97
110
|
if (componentResponse.canStream() || response.writableEnded)
|
|
98
111
|
return;
|
|
99
|
-
writeHeadToServerResponse(response, componentResponse, didError);
|
|
112
|
+
writeHeadToServerResponse(request, response, componentResponse, log, didError);
|
|
100
113
|
if (isRedirect(response)) {
|
|
101
114
|
// Redirects found after any async code
|
|
102
115
|
return response.end();
|
|
@@ -110,7 +123,7 @@ const renderHydrogen = (App, hook) => {
|
|
|
110
123
|
}
|
|
111
124
|
}
|
|
112
125
|
else {
|
|
113
|
-
startWritingHtmlToServerResponse(response,
|
|
126
|
+
startWritingHtmlToServerResponse(response, pipe, dev ? didError : undefined);
|
|
114
127
|
}
|
|
115
128
|
},
|
|
116
129
|
onError(error) {
|
|
@@ -120,15 +133,19 @@ const renderHydrogen = (App, hook) => {
|
|
|
120
133
|
// Delay this error until headers are properly sent.
|
|
121
134
|
response.write(getErrorMarkup(error));
|
|
122
135
|
}
|
|
123
|
-
|
|
136
|
+
log.error(error);
|
|
124
137
|
},
|
|
125
138
|
});
|
|
126
|
-
setTimeout(
|
|
139
|
+
const streamTimeout = setTimeout(() => {
|
|
140
|
+
const errorMessage = `The app failed to stream after ${STREAM_ABORT_TIMEOUT_MS} ms`;
|
|
141
|
+
log.warn(errorMessage);
|
|
142
|
+
}, STREAM_ABORT_TIMEOUT_MS);
|
|
127
143
|
};
|
|
128
144
|
/**
|
|
129
145
|
* Stream a hydration response to the client.
|
|
130
146
|
*/
|
|
131
147
|
const hydrate = function (url, { context, request, response, dev }) {
|
|
148
|
+
const log = getLoggerFromContext(request);
|
|
132
149
|
const state = JSON.parse(url.searchParams.get('state') || '{}');
|
|
133
150
|
const { ReactApp, componentResponse } = buildReactApp({
|
|
134
151
|
App,
|
|
@@ -136,48 +153,65 @@ const renderHydrogen = (App, hook) => {
|
|
|
136
153
|
context,
|
|
137
154
|
request,
|
|
138
155
|
dev,
|
|
156
|
+
log,
|
|
157
|
+
pages,
|
|
139
158
|
});
|
|
140
159
|
response.socket.on('error', (error) => {
|
|
141
|
-
|
|
160
|
+
log.fatal(error);
|
|
142
161
|
});
|
|
143
162
|
let didError;
|
|
144
163
|
const writer = new HydrationWriter();
|
|
145
|
-
const {
|
|
146
|
-
React.createElement(ReactApp, { ...state })),
|
|
164
|
+
const { pipe } = renderToPipeableStream(React.createElement(HydrationContext.Provider, { value: true },
|
|
165
|
+
React.createElement(ReactApp, { ...state })), {
|
|
147
166
|
/**
|
|
148
167
|
* When hydrating, we have to wait until `onCompleteAll` to avoid having
|
|
149
168
|
* `template` and `script` tags inserted and rendered as part of the hydration response.
|
|
150
169
|
*/
|
|
151
170
|
onCompleteAll() {
|
|
171
|
+
clearTimeout(renderTimeout);
|
|
152
172
|
// Tell React to start writing to the writer
|
|
153
|
-
|
|
173
|
+
pipe(writer);
|
|
154
174
|
// Tell React that the writer is ready to drain, which sometimes results in a last "chunk" being written.
|
|
155
175
|
writer.drain();
|
|
156
176
|
response.statusCode = didError ? 500 : 200;
|
|
157
177
|
response.setHeader(getCacheControlHeader({ dev }), componentResponse.cacheControlHeader);
|
|
158
178
|
response.end(generateWireSyntaxFromRenderedHtml(writer.toString()));
|
|
179
|
+
logServerResponse('rsc', log, request, response.statusCode);
|
|
159
180
|
},
|
|
160
181
|
onError(error) {
|
|
161
182
|
didError = error;
|
|
162
|
-
|
|
183
|
+
log.error(error);
|
|
163
184
|
},
|
|
164
185
|
});
|
|
165
|
-
setTimeout(
|
|
186
|
+
const renderTimeout = setTimeout(() => {
|
|
187
|
+
log.error(`The app failed to render RSC after ${STREAM_ABORT_TIMEOUT_MS} ms`);
|
|
188
|
+
}, STREAM_ABORT_TIMEOUT_MS);
|
|
166
189
|
};
|
|
190
|
+
function getApiRoute(url) {
|
|
191
|
+
const routes = getApiRoutesFromPages(pages);
|
|
192
|
+
return getApiRouteFromURL(url, routes);
|
|
193
|
+
}
|
|
167
194
|
return {
|
|
168
195
|
render,
|
|
169
196
|
stream,
|
|
170
197
|
hydrate,
|
|
198
|
+
getApiRoute,
|
|
199
|
+
log,
|
|
171
200
|
};
|
|
172
201
|
};
|
|
173
|
-
function buildReactApp({ App, state, context, request, dev, }) {
|
|
202
|
+
function buildReactApp({ App, state, context, request, dev, log, pages, }) {
|
|
203
|
+
const renderCache = {};
|
|
174
204
|
const helmetContext = {};
|
|
175
205
|
const componentResponse = new ServerComponentResponse();
|
|
176
|
-
const
|
|
206
|
+
const hydrogenServerProps = {
|
|
207
|
+
request,
|
|
208
|
+
response: componentResponse,
|
|
209
|
+
log,
|
|
210
|
+
};
|
|
177
211
|
const ReactApp = (props) => (React.createElement(RenderCacheProvider, { cache: renderCache },
|
|
178
212
|
React.createElement(StaticRouter, { location: { pathname: state.pathname, search: state.search }, context: context },
|
|
179
213
|
React.createElement(HelmetProvider, { context: helmetContext },
|
|
180
|
-
React.createElement(App, { ...props,
|
|
214
|
+
React.createElement(App, { ...props, ...hydrogenServerProps, pages: pages })))));
|
|
181
215
|
return { helmetContext, ReactApp, componentResponse };
|
|
182
216
|
}
|
|
183
217
|
function extractHeadElements(helmetContext) {
|
|
@@ -203,27 +237,31 @@ function supportsReadableStream() {
|
|
|
203
237
|
return false;
|
|
204
238
|
}
|
|
205
239
|
}
|
|
206
|
-
async function renderApp(ReactApp, state, isReactHydrationRequest) {
|
|
240
|
+
async function renderApp(ReactApp, state, log, isReactHydrationRequest) {
|
|
207
241
|
/**
|
|
208
242
|
* Temporary workaround until all Worker runtimes support ReadableStream
|
|
209
243
|
*/
|
|
210
244
|
if (isWorker && !supportsReadableStream()) {
|
|
211
|
-
return renderAppFromStringWithPrepass(ReactApp, state, isReactHydrationRequest);
|
|
245
|
+
return renderAppFromStringWithPrepass(ReactApp, state, log, isReactHydrationRequest);
|
|
212
246
|
}
|
|
213
247
|
const app = isReactHydrationRequest ? (React.createElement(HydrationContext.Provider, { value: true },
|
|
214
248
|
React.createElement(ReactApp, { ...state }))) : (React.createElement(ReactApp, { ...state }));
|
|
215
|
-
return renderAppFromBufferedStream(app, isReactHydrationRequest);
|
|
249
|
+
return renderAppFromBufferedStream(app, log, isReactHydrationRequest);
|
|
216
250
|
}
|
|
217
|
-
function renderAppFromBufferedStream(app, isReactHydrationRequest) {
|
|
251
|
+
function renderAppFromBufferedStream(app, log, isReactHydrationRequest) {
|
|
218
252
|
return new Promise((resolve, reject) => {
|
|
253
|
+
const errorTimeout = setTimeout(() => {
|
|
254
|
+
log.warn(`The app failed to SSR after ${STREAM_ABORT_TIMEOUT_MS} ms`);
|
|
255
|
+
}, STREAM_ABORT_TIMEOUT_MS);
|
|
219
256
|
if (isWorker) {
|
|
220
257
|
let isComplete = false;
|
|
221
258
|
const stream = renderToReadableStream(app, {
|
|
222
259
|
onCompleteAll() {
|
|
260
|
+
clearTimeout(errorTimeout);
|
|
223
261
|
isComplete = true;
|
|
224
262
|
},
|
|
225
263
|
onError(error) {
|
|
226
|
-
|
|
264
|
+
log.error(error);
|
|
227
265
|
reject(error);
|
|
228
266
|
},
|
|
229
267
|
});
|
|
@@ -254,14 +292,15 @@ function renderAppFromBufferedStream(app, isReactHydrationRequest) {
|
|
|
254
292
|
}
|
|
255
293
|
else {
|
|
256
294
|
const writer = new HydrationWriter();
|
|
257
|
-
const {
|
|
295
|
+
const { pipe } = renderToPipeableStream(app, {
|
|
258
296
|
/**
|
|
259
297
|
* When hydrating, we have to wait until `onCompleteAll` to avoid having
|
|
260
298
|
* `template` and `script` tags inserted and rendered as part of the hydration response.
|
|
261
299
|
*/
|
|
262
300
|
onCompleteAll() {
|
|
301
|
+
clearTimeout(errorTimeout);
|
|
263
302
|
// Tell React to start writing to the writer
|
|
264
|
-
|
|
303
|
+
pipe(writer);
|
|
265
304
|
// Tell React that the writer is ready to drain, which sometimes results in a last "chunk" being written.
|
|
266
305
|
writer.drain();
|
|
267
306
|
if (isReactHydrationRequest) {
|
|
@@ -272,7 +311,7 @@ function renderAppFromBufferedStream(app, isReactHydrationRequest) {
|
|
|
272
311
|
}
|
|
273
312
|
},
|
|
274
313
|
onError(error) {
|
|
275
|
-
|
|
314
|
+
log.error(error);
|
|
276
315
|
reject(error);
|
|
277
316
|
},
|
|
278
317
|
});
|
|
@@ -288,7 +327,7 @@ function renderAppFromBufferedStream(app, isReactHydrationRequest) {
|
|
|
288
327
|
* use ssr-prepass to fetch all the queries once, store
|
|
289
328
|
* the results in a context object, and re-render.
|
|
290
329
|
*/
|
|
291
|
-
async function renderAppFromStringWithPrepass(ReactApp, state, isReactHydrationRequest) {
|
|
330
|
+
async function renderAppFromStringWithPrepass(ReactApp, state, log, isReactHydrationRequest) {
|
|
292
331
|
const app = isReactHydrationRequest ? (React.createElement(HydrationContext.Provider, { value: true },
|
|
293
332
|
React.createElement(ReactApp, { ...state }))) : (React.createElement(ReactApp, { ...state }));
|
|
294
333
|
await ssrPrepass(app);
|
|
@@ -298,18 +337,18 @@ async function renderAppFromStringWithPrepass(ReactApp, state, isReactHydrationR
|
|
|
298
337
|
: body;
|
|
299
338
|
}
|
|
300
339
|
export default renderHydrogen;
|
|
301
|
-
function startWritingHtmlToServerResponse(response,
|
|
340
|
+
function startWritingHtmlToServerResponse(response, pipe, error) {
|
|
302
341
|
if (!response.headersSent) {
|
|
303
342
|
response.setHeader('Content-type', 'text/html');
|
|
304
343
|
response.write('<!DOCTYPE html>');
|
|
305
344
|
}
|
|
306
|
-
|
|
345
|
+
pipe(response);
|
|
307
346
|
if (error) {
|
|
308
347
|
// This error was delayed until the headers were properly sent.
|
|
309
348
|
response.write(getErrorMarkup(error));
|
|
310
349
|
}
|
|
311
350
|
}
|
|
312
|
-
function writeHeadToServerResponse(response, { headers, status, customStatus }, error) {
|
|
351
|
+
function writeHeadToServerResponse(request, response, { headers, status, customStatus }, log, error) {
|
|
313
352
|
var _a, _b;
|
|
314
353
|
if (response.headersSent)
|
|
315
354
|
return;
|
|
@@ -323,6 +362,7 @@ function writeHeadToServerResponse(response, { headers, status, customStatus },
|
|
|
323
362
|
response.statusMessage = customStatus.text;
|
|
324
363
|
}
|
|
325
364
|
}
|
|
365
|
+
logServerResponse('str', log, request, response.statusCode);
|
|
326
366
|
}
|
|
327
367
|
function isRedirect(response) {
|
|
328
368
|
return response.statusCode >= 300 && response.statusCode < 400;
|
|
@@ -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,15 +1,17 @@
|
|
|
1
1
|
import { ReactElement } from 'react';
|
|
2
|
-
|
|
2
|
+
import { Logger } from '../../utilities/log/log';
|
|
3
|
+
import type { ImportGlobEagerOutput } from '../../types';
|
|
3
4
|
/**
|
|
4
5
|
* Build a set of default Hydrogen routes based on the output provided by Vite's
|
|
5
6
|
* import.meta.globEager method.
|
|
6
7
|
*
|
|
7
8
|
* @see https://vitejs.dev/guide/features.html#glob-import
|
|
8
9
|
*/
|
|
9
|
-
export declare function DefaultRoutes({ pages, serverState, fallback, }: {
|
|
10
|
+
export declare function DefaultRoutes({ pages, serverState, fallback, log, }: {
|
|
10
11
|
pages: ImportGlobEagerOutput;
|
|
11
12
|
serverState: Record<string, any>;
|
|
12
13
|
fallback?: ReactElement;
|
|
14
|
+
log: Logger;
|
|
13
15
|
}): JSX.Element;
|
|
14
16
|
interface HydrogenRoute {
|
|
15
17
|
component: any;
|
|
@@ -6,17 +6,18 @@ import { Route, Switch, useRouteMatch } from 'react-router-dom';
|
|
|
6
6
|
*
|
|
7
7
|
* @see https://vitejs.dev/guide/features.html#glob-import
|
|
8
8
|
*/
|
|
9
|
-
export function DefaultRoutes({ pages, serverState, fallback, }) {
|
|
9
|
+
export function DefaultRoutes({ pages, serverState, fallback, log, }) {
|
|
10
10
|
const { path } = useRouteMatch();
|
|
11
11
|
const routes = useMemo(() => createRoutesFromPages(pages, path), [pages, path]);
|
|
12
12
|
return (React.createElement(Switch, null,
|
|
13
13
|
routes.map((route) => (React.createElement(Route, { key: route.path, exact: route.exact, path: route.path },
|
|
14
|
-
React.createElement(route.component, { ...serverState })))),
|
|
14
|
+
React.createElement(route.component, { ...serverState, log: log })))),
|
|
15
15
|
fallback && React.createElement(Route, { path: "*" }, fallback)));
|
|
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,6 +1,7 @@
|
|
|
1
1
|
import React, { createContext, useMemo, useCallback,
|
|
2
2
|
// @ts-ignore
|
|
3
3
|
useTransition, } from 'react';
|
|
4
|
+
const PRIVATE_PROPS = ['request', 'response'];
|
|
4
5
|
export const ServerStateContext = createContext(null);
|
|
5
6
|
export function ServerStateProvider({ serverState, setServerState, children, }) {
|
|
6
7
|
const [pending, startTransition] = useTransition();
|
|
@@ -25,8 +26,9 @@ export function ServerStateProvider({ serverState, setServerState, children, })
|
|
|
25
26
|
newValue = input;
|
|
26
27
|
}
|
|
27
28
|
if (__DEV__) {
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
const privateProp = PRIVATE_PROPS.find((prop) => prop in newValue);
|
|
30
|
+
if (privateProp) {
|
|
31
|
+
console.warn(`Custom "${privateProp}" property in server state is ignored. Use a different name.`);
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
return {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { log } from '../../utilities/log';
|
|
1
2
|
import { deleteItemFromCache, getItemFromCache, isStale, setItemInCache, } from '../../framework/cache';
|
|
2
3
|
import { runDelayedFunction } from '../../framework/runtime';
|
|
3
4
|
import { useRenderCacheData } from '../RenderCacheProvider/hook';
|
|
@@ -13,7 +14,8 @@ key,
|
|
|
13
14
|
queryFn,
|
|
14
15
|
/** Options including `cache` to manage the cache behavior of the sub-request. */
|
|
15
16
|
queryOptions) {
|
|
16
|
-
|
|
17
|
+
const withCacheIdKey = ['__QUERY_CACHE_ID__', ...key];
|
|
18
|
+
return useRenderCacheData(withCacheIdKey, cachedQueryFnBuilder(withCacheIdKey, queryFn, queryOptions));
|
|
17
19
|
}
|
|
18
20
|
function cachedQueryFnBuilder(key, queryFn, queryOptions) {
|
|
19
21
|
const resolvedQueryOptions = {
|
|
@@ -33,10 +35,10 @@ function cachedQueryFnBuilder(key, queryFn, queryOptions) {
|
|
|
33
35
|
* Important: Do this async
|
|
34
36
|
*/
|
|
35
37
|
if (isStale(response)) {
|
|
36
|
-
|
|
38
|
+
log.debug('[useQuery] cache stale; generating new response in background');
|
|
37
39
|
const lockKey = `lock-${key}`;
|
|
38
40
|
runDelayedFunction(async () => {
|
|
39
|
-
|
|
41
|
+
log.debug(`[stale regen] fetching cache lock`);
|
|
40
42
|
const lockExists = await getItemFromCache(lockKey);
|
|
41
43
|
if (lockExists)
|
|
42
44
|
return;
|
|
@@ -46,7 +48,7 @@ function cachedQueryFnBuilder(key, queryFn, queryOptions) {
|
|
|
46
48
|
await setItemInCache(key, output, resolvedQueryOptions === null || resolvedQueryOptions === void 0 ? void 0 : resolvedQueryOptions.cache);
|
|
47
49
|
}
|
|
48
50
|
catch (e) {
|
|
49
|
-
|
|
51
|
+
log.error(`Error generating async response: ${e.message}`);
|
|
50
52
|
}
|
|
51
53
|
finally {
|
|
52
54
|
await deleteItemFromCache(lockKey);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ShopifyProviderValue } from '../ShopifyProvider/types';
|
|
2
2
|
/**
|
|
3
|
-
* The `useShop` hook provides access to values within `shopify.config.js`.
|
|
3
|
+
* The `useShop` hook provides access to values within `shopify.config.js`. It must be a descendent of a `ShopifyProvider` component.
|
|
4
4
|
*/
|
|
5
5
|
export declare function useShop(): ShopifyProviderValue;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useContext } from 'react';
|
|
2
2
|
import { ShopifyContext } from '../ShopifyProvider/ShopifyContext';
|
|
3
3
|
/**
|
|
4
|
-
* The `useShop` hook provides access to values within `shopify.config.js`.
|
|
4
|
+
* The `useShop` hook provides access to values within `shopify.config.js`. It must be a descendent of a `ShopifyProvider` component.
|
|
5
5
|
*/
|
|
6
6
|
export function useShop() {
|
|
7
7
|
const context = useContext(ShopifyContext);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getTime } from '../../utilities/timing';
|
|
1
2
|
/**
|
|
2
3
|
* This augments the `Request` object from the Fetch API:
|
|
3
4
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/Request
|
|
@@ -14,8 +15,12 @@ export class ServerComponentRequest extends Request {
|
|
|
14
15
|
super(getUrlFromNodeRequest(input), {
|
|
15
16
|
headers: new Headers(input.headers),
|
|
16
17
|
method: input.method,
|
|
18
|
+
body: input.method !== 'GET' && input.method !== 'HEAD'
|
|
19
|
+
? input.body
|
|
20
|
+
: undefined,
|
|
17
21
|
});
|
|
18
22
|
}
|
|
23
|
+
this.time = getTime();
|
|
19
24
|
this.cookies = this.parseCookies();
|
|
20
25
|
}
|
|
21
26
|
parseCookies() {
|
|
@@ -16,32 +16,24 @@ 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`,
|
|
33
22
|
* which is needed for proxy requests and server-side API requests.
|
|
34
23
|
*/
|
|
35
24
|
if (!globalThis.fetch) {
|
|
36
|
-
const fetch = await import('
|
|
25
|
+
const { fetch, Request, Response, Headers } = await import('undici');
|
|
26
|
+
const { default: AbortController } = await import('abort-controller');
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
globalThis.fetch = fetch;
|
|
37
29
|
// @ts-ignore
|
|
38
|
-
globalThis.
|
|
30
|
+
globalThis.Request = Request;
|
|
39
31
|
// @ts-ignore
|
|
40
|
-
globalThis.
|
|
32
|
+
globalThis.Response = Response;
|
|
41
33
|
// @ts-ignore
|
|
42
|
-
globalThis.
|
|
34
|
+
globalThis.Headers = Headers;
|
|
43
35
|
// @ts-ignore
|
|
44
|
-
globalThis.
|
|
36
|
+
globalThis.AbortController = AbortController;
|
|
45
37
|
}
|
|
46
38
|
/**
|
|
47
39
|
* Dynamically import ServerComponentResponse after the `fetch`
|
|
@@ -69,7 +61,18 @@ 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
|
+
const reader = eventResponse.body.getReader();
|
|
66
|
+
while (true) {
|
|
67
|
+
const { done, value } = await reader.read();
|
|
68
|
+
if (done)
|
|
69
|
+
return response.end();
|
|
70
|
+
response.write(value);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
response.end();
|
|
75
|
+
}
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
catch (e) {
|
|
@@ -99,11 +102,6 @@ export function hydrogenMiddleware({ dev, cache, indexTemplate, getServerEntrypo
|
|
|
99
102
|
}
|
|
100
103
|
};
|
|
101
104
|
}
|
|
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
105
|
/**
|
|
108
106
|
* /graphiql and /___graphql are supported
|
|
109
107
|
*/
|
|
@@ -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
|
};
|