@shopify/hydrogen 0.8.0 → 0.8.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/CartLineQuantityAdjustButton/CartLineQuantityAdjustButton.js +4 -0
- package/dist/esnext/entry-server.js +70 -37
- package/dist/esnext/foundation/Router/DefaultRoutes.d.ts +3 -1
- package/dist/esnext/foundation/Router/DefaultRoutes.js +2 -2
- package/dist/esnext/foundation/ServerStateProvider/ServerStateProvider.client.js +4 -2
- package/dist/esnext/foundation/useQuery/hooks.js +4 -3
- 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 +2 -0
- 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 +15 -1
- package/dist/esnext/handle-event.js +74 -10
- package/dist/esnext/hooks/useShopQuery/hooks.d.ts +1 -3
- package/dist/esnext/hooks/useShopQuery/hooks.js +6 -5
- package/dist/esnext/index.d.ts +1 -1
- package/dist/esnext/index.js +1 -1
- package/dist/esnext/types.d.ts +4 -0
- package/dist/esnext/utilities/index.d.ts +2 -0
- package/dist/esnext/utilities/index.js +2 -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 +20 -0
- package/dist/esnext/utilities/log/log.js +71 -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 +2 -0
- 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 +15 -1
- package/dist/node/handle-event.js +74 -10
- package/dist/node/types.d.ts +4 -0
- package/dist/node/utilities/index.d.ts +2 -0
- package/dist/node/utilities/index.js +9 -1
- package/dist/node/utilities/log/index.d.ts +1 -0
- package/dist/node/utilities/log/index.js +9 -0
- package/dist/node/utilities/log/log.d.ts +20 -0
- package/dist/node/utilities/log/log.js +78 -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 +2 -0
- package/dist/worker/handle-event.js +74 -10
- package/dist/worker/types.d.ts +4 -0
- package/dist/worker/utilities/log/index.d.ts +1 -0
- package/dist/worker/utilities/log/index.js +1 -0
- package/dist/worker/utilities/log/log.d.ts +20 -0
- package/dist/worker/utilities/log/log.js +71 -0
- package/dist/worker/utilities/timing.d.ts +7 -0
- package/dist/worker/utilities/timing.js +14 -0
- package/package.json +6 -6
|
@@ -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,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 } 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';
|
|
@@ -32,7 +36,7 @@ const renderHydrogen = (App, hook) => {
|
|
|
32
36
|
* and returning any initial state that needs to be hydrated into the client version of the app.
|
|
33
37
|
* NOTE: This is currently only used for SEO bots or Worker runtime (where Stream is not yet supported).
|
|
34
38
|
*/
|
|
35
|
-
const render = async function (url, { context, request, isReactHydrationRequest, dev }) {
|
|
39
|
+
const render = async function (url, { context, request, isReactHydrationRequest, dev, log }) {
|
|
36
40
|
var _a, _b;
|
|
37
41
|
const state = isReactHydrationRequest
|
|
38
42
|
? JSON.parse((_b = (_a = url.searchParams) === null || _a === void 0 ? void 0 : _a.get('state')) !== null && _b !== void 0 ? _b : '{}')
|
|
@@ -43,8 +47,9 @@ const renderHydrogen = (App, hook) => {
|
|
|
43
47
|
context,
|
|
44
48
|
request,
|
|
45
49
|
dev,
|
|
50
|
+
log,
|
|
46
51
|
});
|
|
47
|
-
const body = await renderApp(ReactApp, state, isReactHydrationRequest);
|
|
52
|
+
const body = await renderApp(ReactApp, state, log, isReactHydrationRequest);
|
|
48
53
|
if (componentResponse.customBody) {
|
|
49
54
|
return { body: await componentResponse.customBody, url, componentResponse };
|
|
50
55
|
}
|
|
@@ -61,7 +66,7 @@ const renderHydrogen = (App, hook) => {
|
|
|
61
66
|
* Stream a response to the client. NOTE: This omits custom `<head>`
|
|
62
67
|
* information, so this method should not be used by crawlers.
|
|
63
68
|
*/
|
|
64
|
-
const stream = function (url, { context, request, response, template, dev }) {
|
|
69
|
+
const stream = function (url, { context, request, response, template, dev, log }) {
|
|
65
70
|
const state = { pathname: url.pathname, search: url.search };
|
|
66
71
|
const { ReactApp, componentResponse } = buildReactApp({
|
|
67
72
|
App,
|
|
@@ -69,34 +74,36 @@ const renderHydrogen = (App, hook) => {
|
|
|
69
74
|
context,
|
|
70
75
|
request,
|
|
71
76
|
dev,
|
|
77
|
+
log,
|
|
72
78
|
});
|
|
73
79
|
response.socket.on('error', (error) => {
|
|
74
|
-
|
|
80
|
+
log.fatal(error);
|
|
75
81
|
});
|
|
76
82
|
let didError;
|
|
77
83
|
const head = template.match(/<head>(.+?)<\/head>/s)[1];
|
|
78
|
-
const {
|
|
79
|
-
React.createElement(ReactApp, { ...state })),
|
|
80
|
-
|
|
84
|
+
const { pipe, abort } = renderToPipeableStream(React.createElement(Html, { head: head },
|
|
85
|
+
React.createElement(ReactApp, { ...state })), {
|
|
86
|
+
onCompleteShell() {
|
|
81
87
|
/**
|
|
82
88
|
* TODO: This assumes `response.cache()` has been called _before_ any
|
|
83
89
|
* queries which might be caught behind Suspense. Clarify this or add
|
|
84
90
|
* additional checks downstream?
|
|
85
91
|
*/
|
|
86
92
|
response.setHeader(getCacheControlHeader({ dev }), componentResponse.cacheControlHeader);
|
|
87
|
-
writeHeadToServerResponse(response, componentResponse, didError);
|
|
93
|
+
writeHeadToServerResponse(request, response, componentResponse, log, didError);
|
|
88
94
|
if (isRedirect(response)) {
|
|
89
95
|
// Return redirects early without further rendering/streaming
|
|
90
96
|
return response.end();
|
|
91
97
|
}
|
|
92
98
|
if (!componentResponse.canStream())
|
|
93
99
|
return;
|
|
94
|
-
startWritingHtmlToServerResponse(response,
|
|
100
|
+
startWritingHtmlToServerResponse(response, pipe, dev ? didError : undefined);
|
|
95
101
|
},
|
|
96
102
|
onCompleteAll() {
|
|
103
|
+
clearTimeout(streamTimeout);
|
|
97
104
|
if (componentResponse.canStream() || response.writableEnded)
|
|
98
105
|
return;
|
|
99
|
-
writeHeadToServerResponse(response, componentResponse, didError);
|
|
106
|
+
writeHeadToServerResponse(request, response, componentResponse, log, didError);
|
|
100
107
|
if (isRedirect(response)) {
|
|
101
108
|
// Redirects found after any async code
|
|
102
109
|
return response.end();
|
|
@@ -110,7 +117,7 @@ const renderHydrogen = (App, hook) => {
|
|
|
110
117
|
}
|
|
111
118
|
}
|
|
112
119
|
else {
|
|
113
|
-
startWritingHtmlToServerResponse(response,
|
|
120
|
+
startWritingHtmlToServerResponse(response, pipe, dev ? didError : undefined);
|
|
114
121
|
}
|
|
115
122
|
},
|
|
116
123
|
onError(error) {
|
|
@@ -120,15 +127,22 @@ const renderHydrogen = (App, hook) => {
|
|
|
120
127
|
// Delay this error until headers are properly sent.
|
|
121
128
|
response.write(getErrorMarkup(error));
|
|
122
129
|
}
|
|
123
|
-
|
|
130
|
+
log.error(error);
|
|
124
131
|
},
|
|
125
132
|
});
|
|
126
|
-
setTimeout(
|
|
133
|
+
const streamTimeout = setTimeout(() => {
|
|
134
|
+
const errorMessage = `The app failed to stream after ${STREAM_ABORT_TIMEOUT_MS} ms`;
|
|
135
|
+
log.error(errorMessage);
|
|
136
|
+
if (dev && response.headersSent) {
|
|
137
|
+
response.write(getErrorMarkup(new Error(errorMessage)));
|
|
138
|
+
}
|
|
139
|
+
abort();
|
|
140
|
+
}, STREAM_ABORT_TIMEOUT_MS);
|
|
127
141
|
};
|
|
128
142
|
/**
|
|
129
143
|
* Stream a hydration response to the client.
|
|
130
144
|
*/
|
|
131
|
-
const hydrate = function (url, { context, request, response, dev }) {
|
|
145
|
+
const hydrate = function (url, { context, request, response, dev, log }) {
|
|
132
146
|
const state = JSON.parse(url.searchParams.get('state') || '{}');
|
|
133
147
|
const { ReactApp, componentResponse } = buildReactApp({
|
|
134
148
|
App,
|
|
@@ -136,33 +150,41 @@ const renderHydrogen = (App, hook) => {
|
|
|
136
150
|
context,
|
|
137
151
|
request,
|
|
138
152
|
dev,
|
|
153
|
+
log,
|
|
139
154
|
});
|
|
140
155
|
response.socket.on('error', (error) => {
|
|
141
|
-
|
|
156
|
+
log.fatal(error);
|
|
142
157
|
});
|
|
143
158
|
let didError;
|
|
144
159
|
const writer = new HydrationWriter();
|
|
145
|
-
const {
|
|
146
|
-
React.createElement(ReactApp, { ...state })),
|
|
160
|
+
const { pipe, abort } = renderToPipeableStream(React.createElement(HydrationContext.Provider, { value: true },
|
|
161
|
+
React.createElement(ReactApp, { ...state })), {
|
|
147
162
|
/**
|
|
148
163
|
* When hydrating, we have to wait until `onCompleteAll` to avoid having
|
|
149
164
|
* `template` and `script` tags inserted and rendered as part of the hydration response.
|
|
150
165
|
*/
|
|
151
166
|
onCompleteAll() {
|
|
167
|
+
clearTimeout(renderTimeout);
|
|
152
168
|
// Tell React to start writing to the writer
|
|
153
|
-
|
|
169
|
+
pipe(writer);
|
|
154
170
|
// Tell React that the writer is ready to drain, which sometimes results in a last "chunk" being written.
|
|
155
171
|
writer.drain();
|
|
156
172
|
response.statusCode = didError ? 500 : 200;
|
|
157
173
|
response.setHeader(getCacheControlHeader({ dev }), componentResponse.cacheControlHeader);
|
|
158
174
|
response.end(generateWireSyntaxFromRenderedHtml(writer.toString()));
|
|
175
|
+
logServerResponse('rsc', log, request, response.statusCode);
|
|
159
176
|
},
|
|
160
177
|
onError(error) {
|
|
161
178
|
didError = error;
|
|
162
|
-
|
|
179
|
+
log.error(error);
|
|
163
180
|
},
|
|
164
181
|
});
|
|
165
|
-
setTimeout(
|
|
182
|
+
const renderTimeout = setTimeout(() => {
|
|
183
|
+
const errorMessage = `The app failed to render RSC after ${STREAM_ABORT_TIMEOUT_MS} ms`;
|
|
184
|
+
didError = new Error(errorMessage);
|
|
185
|
+
log.error(errorMessage);
|
|
186
|
+
abort();
|
|
187
|
+
}, STREAM_ABORT_TIMEOUT_MS);
|
|
166
188
|
};
|
|
167
189
|
return {
|
|
168
190
|
render,
|
|
@@ -170,14 +192,19 @@ const renderHydrogen = (App, hook) => {
|
|
|
170
192
|
hydrate,
|
|
171
193
|
};
|
|
172
194
|
};
|
|
173
|
-
function buildReactApp({ App, state, context, request, dev, }) {
|
|
195
|
+
function buildReactApp({ App, state, context, request, dev, log, }) {
|
|
196
|
+
const renderCache = {};
|
|
174
197
|
const helmetContext = {};
|
|
175
198
|
const componentResponse = new ServerComponentResponse();
|
|
176
|
-
const
|
|
199
|
+
const hydrogenServerProps = {
|
|
200
|
+
request,
|
|
201
|
+
response: componentResponse,
|
|
202
|
+
log,
|
|
203
|
+
};
|
|
177
204
|
const ReactApp = (props) => (React.createElement(RenderCacheProvider, { cache: renderCache },
|
|
178
205
|
React.createElement(StaticRouter, { location: { pathname: state.pathname, search: state.search }, context: context },
|
|
179
206
|
React.createElement(HelmetProvider, { context: helmetContext },
|
|
180
|
-
React.createElement(App, { ...props,
|
|
207
|
+
React.createElement(App, { ...props, ...hydrogenServerProps })))));
|
|
181
208
|
return { helmetContext, ReactApp, componentResponse };
|
|
182
209
|
}
|
|
183
210
|
function extractHeadElements(helmetContext) {
|
|
@@ -203,27 +230,31 @@ function supportsReadableStream() {
|
|
|
203
230
|
return false;
|
|
204
231
|
}
|
|
205
232
|
}
|
|
206
|
-
async function renderApp(ReactApp, state, isReactHydrationRequest) {
|
|
233
|
+
async function renderApp(ReactApp, state, log, isReactHydrationRequest) {
|
|
207
234
|
/**
|
|
208
235
|
* Temporary workaround until all Worker runtimes support ReadableStream
|
|
209
236
|
*/
|
|
210
237
|
if (isWorker && !supportsReadableStream()) {
|
|
211
|
-
return renderAppFromStringWithPrepass(ReactApp, state, isReactHydrationRequest);
|
|
238
|
+
return renderAppFromStringWithPrepass(ReactApp, state, log, isReactHydrationRequest);
|
|
212
239
|
}
|
|
213
240
|
const app = isReactHydrationRequest ? (React.createElement(HydrationContext.Provider, { value: true },
|
|
214
241
|
React.createElement(ReactApp, { ...state }))) : (React.createElement(ReactApp, { ...state }));
|
|
215
|
-
return renderAppFromBufferedStream(app, isReactHydrationRequest);
|
|
242
|
+
return renderAppFromBufferedStream(app, log, isReactHydrationRequest);
|
|
216
243
|
}
|
|
217
|
-
function renderAppFromBufferedStream(app, isReactHydrationRequest) {
|
|
244
|
+
function renderAppFromBufferedStream(app, log, isReactHydrationRequest) {
|
|
218
245
|
return new Promise((resolve, reject) => {
|
|
246
|
+
const errorTimeout = setTimeout(() => {
|
|
247
|
+
reject(new Error(`The app failed to SSR after ${STREAM_ABORT_TIMEOUT_MS} ms`));
|
|
248
|
+
}, STREAM_ABORT_TIMEOUT_MS);
|
|
219
249
|
if (isWorker) {
|
|
220
250
|
let isComplete = false;
|
|
221
251
|
const stream = renderToReadableStream(app, {
|
|
222
252
|
onCompleteAll() {
|
|
253
|
+
clearTimeout(errorTimeout);
|
|
223
254
|
isComplete = true;
|
|
224
255
|
},
|
|
225
256
|
onError(error) {
|
|
226
|
-
|
|
257
|
+
log.error(error);
|
|
227
258
|
reject(error);
|
|
228
259
|
},
|
|
229
260
|
});
|
|
@@ -254,14 +285,15 @@ function renderAppFromBufferedStream(app, isReactHydrationRequest) {
|
|
|
254
285
|
}
|
|
255
286
|
else {
|
|
256
287
|
const writer = new HydrationWriter();
|
|
257
|
-
const {
|
|
288
|
+
const { pipe } = renderToPipeableStream(app, {
|
|
258
289
|
/**
|
|
259
290
|
* When hydrating, we have to wait until `onCompleteAll` to avoid having
|
|
260
291
|
* `template` and `script` tags inserted and rendered as part of the hydration response.
|
|
261
292
|
*/
|
|
262
293
|
onCompleteAll() {
|
|
294
|
+
clearTimeout(errorTimeout);
|
|
263
295
|
// Tell React to start writing to the writer
|
|
264
|
-
|
|
296
|
+
pipe(writer);
|
|
265
297
|
// Tell React that the writer is ready to drain, which sometimes results in a last "chunk" being written.
|
|
266
298
|
writer.drain();
|
|
267
299
|
if (isReactHydrationRequest) {
|
|
@@ -272,7 +304,7 @@ function renderAppFromBufferedStream(app, isReactHydrationRequest) {
|
|
|
272
304
|
}
|
|
273
305
|
},
|
|
274
306
|
onError(error) {
|
|
275
|
-
|
|
307
|
+
log.error(error);
|
|
276
308
|
reject(error);
|
|
277
309
|
},
|
|
278
310
|
});
|
|
@@ -288,7 +320,7 @@ function renderAppFromBufferedStream(app, isReactHydrationRequest) {
|
|
|
288
320
|
* use ssr-prepass to fetch all the queries once, store
|
|
289
321
|
* the results in a context object, and re-render.
|
|
290
322
|
*/
|
|
291
|
-
async function renderAppFromStringWithPrepass(ReactApp, state, isReactHydrationRequest) {
|
|
323
|
+
async function renderAppFromStringWithPrepass(ReactApp, state, log, isReactHydrationRequest) {
|
|
292
324
|
const app = isReactHydrationRequest ? (React.createElement(HydrationContext.Provider, { value: true },
|
|
293
325
|
React.createElement(ReactApp, { ...state }))) : (React.createElement(ReactApp, { ...state }));
|
|
294
326
|
await ssrPrepass(app);
|
|
@@ -298,18 +330,18 @@ async function renderAppFromStringWithPrepass(ReactApp, state, isReactHydrationR
|
|
|
298
330
|
: body;
|
|
299
331
|
}
|
|
300
332
|
export default renderHydrogen;
|
|
301
|
-
function startWritingHtmlToServerResponse(response,
|
|
333
|
+
function startWritingHtmlToServerResponse(response, pipe, error) {
|
|
302
334
|
if (!response.headersSent) {
|
|
303
335
|
response.setHeader('Content-type', 'text/html');
|
|
304
336
|
response.write('<!DOCTYPE html>');
|
|
305
337
|
}
|
|
306
|
-
|
|
338
|
+
pipe(response);
|
|
307
339
|
if (error) {
|
|
308
340
|
// This error was delayed until the headers were properly sent.
|
|
309
341
|
response.write(getErrorMarkup(error));
|
|
310
342
|
}
|
|
311
343
|
}
|
|
312
|
-
function writeHeadToServerResponse(response, { headers, status, customStatus }, error) {
|
|
344
|
+
function writeHeadToServerResponse(request, response, { headers, status, customStatus }, log, error) {
|
|
313
345
|
var _a, _b;
|
|
314
346
|
if (response.headersSent)
|
|
315
347
|
return;
|
|
@@ -323,6 +355,7 @@ function writeHeadToServerResponse(response, { headers, status, customStatus },
|
|
|
323
355
|
response.statusMessage = customStatus.text;
|
|
324
356
|
}
|
|
325
357
|
}
|
|
358
|
+
logServerResponse('str', log, request, response.statusCode);
|
|
326
359
|
}
|
|
327
360
|
function isRedirect(response) {
|
|
328
361
|
return response.statusCode >= 300 && response.statusCode < 400;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ReactElement } from 'react';
|
|
2
|
+
import { Logger } from '../../utilities/log/log';
|
|
2
3
|
export declare type ImportGlobEagerOutput = Record<string, Record<'default', any>>;
|
|
3
4
|
/**
|
|
4
5
|
* Build a set of default Hydrogen routes based on the output provided by Vite's
|
|
@@ -6,10 +7,11 @@ export declare type ImportGlobEagerOutput = Record<string, Record<'default', any
|
|
|
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,12 +6,12 @@ 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 = '*') {
|
|
@@ -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';
|
|
1
2
|
import { deleteItemFromCache, getItemFromCache, isStale, setItemInCache, } from '../../framework/cache';
|
|
2
3
|
import { runDelayedFunction } from '../../framework/runtime';
|
|
3
4
|
import { useRenderCacheData } from '../RenderCacheProvider/hook';
|
|
@@ -33,10 +34,10 @@ function cachedQueryFnBuilder(key, queryFn, queryOptions) {
|
|
|
33
34
|
* Important: Do this async
|
|
34
35
|
*/
|
|
35
36
|
if (isStale(response)) {
|
|
36
|
-
|
|
37
|
+
log.debug('[useQuery] cache stale; generating new response in background');
|
|
37
38
|
const lockKey = `lock-${key}`;
|
|
38
39
|
runDelayedFunction(async () => {
|
|
39
|
-
|
|
40
|
+
log.debug(`[stale regen] fetching cache lock`);
|
|
40
41
|
const lockExists = await getItemFromCache(lockKey);
|
|
41
42
|
if (lockExists)
|
|
42
43
|
return;
|
|
@@ -46,7 +47,7 @@ function cachedQueryFnBuilder(key, queryFn, queryOptions) {
|
|
|
46
47
|
await setItemInCache(key, output, resolvedQueryOptions === null || resolvedQueryOptions === void 0 ? void 0 : resolvedQueryOptions.cache);
|
|
47
48
|
}
|
|
48
49
|
catch (e) {
|
|
49
|
-
|
|
50
|
+
log.error(`Error generating async response: ${e.message}`);
|
|
50
51
|
}
|
|
51
52
|
finally {
|
|
52
53
|
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
|
|
@@ -16,6 +17,7 @@ export class ServerComponentRequest extends Request {
|
|
|
16
17
|
method: input.method,
|
|
17
18
|
});
|
|
18
19
|
}
|
|
20
|
+
this.time = getTime();
|
|
19
21
|
this.cookies = this.parseCookies();
|
|
20
22
|
}
|
|
21
23
|
parseCookies() {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default () => {
|
|
2
2
|
return {
|
|
3
3
|
name: 'vite-plugin-hydrogen-config',
|
|
4
|
-
config: (
|
|
4
|
+
config: async (config, env) => ({
|
|
5
5
|
resolve: {
|
|
6
6
|
alias: {
|
|
7
7
|
/**
|
|
@@ -66,6 +66,7 @@ export default () => {
|
|
|
66
66
|
define: {
|
|
67
67
|
__DEV__: env.mode !== 'production',
|
|
68
68
|
},
|
|
69
|
+
envPrefix: ['VITE_', 'PUBLIC_'],
|
|
69
70
|
}),
|
|
70
71
|
};
|
|
71
72
|
};
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
2
|
import type { HydrogenVitePluginOptions, ShopifyConfig } from '../../types';
|
|
3
3
|
declare const _default: (shopifyConfig: ShopifyConfig, pluginOptions: HydrogenVitePluginOptions) => Plugin;
|
|
4
4
|
export default _default;
|
|
5
|
+
declare global {
|
|
6
|
+
var Oxygen: {
|
|
7
|
+
env: Record<string, string | undefined>;
|
|
8
|
+
[key: string]: any;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadEnv } from 'vite';
|
|
1
2
|
import path from 'path';
|
|
2
3
|
import { promises as fs } from 'fs';
|
|
3
4
|
import { hydrogenMiddleware, graphiqlMiddleware } from '../middleware';
|
|
@@ -11,12 +12,13 @@ export default (shopifyConfig, pluginOptions) => {
|
|
|
11
12
|
* loading them in an SSR context, rendering them using the `entry-server` endpoint in the
|
|
12
13
|
* user's project, and injecting the static HTML into the template.
|
|
13
14
|
*/
|
|
14
|
-
configureServer(server) {
|
|
15
|
+
async configureServer(server) {
|
|
15
16
|
const resolve = (p) => path.resolve(server.config.root, p);
|
|
16
17
|
async function getIndexTemplate(url) {
|
|
17
18
|
const indexHtml = await fs.readFile(resolve('index.html'), 'utf-8');
|
|
18
19
|
return await server.transformIndexHtml(url, indexHtml);
|
|
19
20
|
}
|
|
21
|
+
await polyfillOxygenEnv(server.config);
|
|
20
22
|
// The default vite middleware rewrites the URL `/graphqil` to `/index.html`
|
|
21
23
|
// By running this middleware first, we avoid that.
|
|
22
24
|
server.middlewares.use(graphiqlMiddleware({
|
|
@@ -36,3 +38,15 @@ export default (shopifyConfig, pluginOptions) => {
|
|
|
36
38
|
},
|
|
37
39
|
};
|
|
38
40
|
};
|
|
41
|
+
async function polyfillOxygenEnv(config) {
|
|
42
|
+
const env = await loadEnv(config.mode, config.root, '');
|
|
43
|
+
const publicPrefixes = Array.isArray(config.envPrefix)
|
|
44
|
+
? config.envPrefix
|
|
45
|
+
: [config.envPrefix || ''];
|
|
46
|
+
for (const key of Object.keys(env)) {
|
|
47
|
+
if (publicPrefixes.some((prefix) => key.startsWith(prefix))) {
|
|
48
|
+
delete env[key];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
globalThis.Oxygen = { env };
|
|
52
|
+
}
|
|
@@ -1,6 +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 { getLoggerFromContext, logServerResponse } from './utilities/log';
|
|
4
5
|
export default async function handleEvent(event, { request, entrypoint, indexTemplate, assetHandler, streamableResponse, dev, cache, context, }) {
|
|
5
6
|
var _a, _b, _c, _d, _e;
|
|
6
7
|
const url = new URL(request.url);
|
|
@@ -27,7 +28,9 @@ export default async function handleEvent(event, { request, entrypoint, indexTem
|
|
|
27
28
|
throw new Error(`entry-server.jsx could not be loaded. This likely occurred because of a Vite compilation error.\n` +
|
|
28
29
|
`Please check your server logs for more information.`);
|
|
29
30
|
}
|
|
30
|
-
const
|
|
31
|
+
const userAgent = request.headers.get('user-agent');
|
|
32
|
+
const isStreamable = streamableResponse && !isBotUA(url, userAgent);
|
|
33
|
+
const logger = getLoggerFromContext(request);
|
|
31
34
|
/**
|
|
32
35
|
* Stream back real-user responses, but for bots/etc,
|
|
33
36
|
* use `render` instead. This is because we need to inject <head>
|
|
@@ -35,7 +38,13 @@ export default async function handleEvent(event, { request, entrypoint, indexTem
|
|
|
35
38
|
*/
|
|
36
39
|
if (isStreamable) {
|
|
37
40
|
if (isReactHydrationRequest) {
|
|
38
|
-
hydrate(url, {
|
|
41
|
+
hydrate(url, {
|
|
42
|
+
context: {},
|
|
43
|
+
request,
|
|
44
|
+
response: streamableResponse,
|
|
45
|
+
dev,
|
|
46
|
+
log: logger,
|
|
47
|
+
});
|
|
39
48
|
}
|
|
40
49
|
else {
|
|
41
50
|
stream(url, {
|
|
@@ -44,11 +53,18 @@ export default async function handleEvent(event, { request, entrypoint, indexTem
|
|
|
44
53
|
response: streamableResponse,
|
|
45
54
|
template,
|
|
46
55
|
dev,
|
|
56
|
+
log: logger,
|
|
47
57
|
});
|
|
48
58
|
}
|
|
49
59
|
return;
|
|
50
60
|
}
|
|
51
|
-
const { body, bodyAttributes, htmlAttributes, componentResponse, ...head } = await render(url, {
|
|
61
|
+
const { body, bodyAttributes, htmlAttributes, componentResponse, ...head } = await render(url, {
|
|
62
|
+
request,
|
|
63
|
+
context: {},
|
|
64
|
+
isReactHydrationRequest,
|
|
65
|
+
dev,
|
|
66
|
+
log: logger,
|
|
67
|
+
});
|
|
52
68
|
const headers = componentResponse.headers;
|
|
53
69
|
/**
|
|
54
70
|
* TODO: Also add `Vary` headers for `accept-language` and any other keys
|
|
@@ -84,15 +100,9 @@ export default async function handleEvent(event, { request, entrypoint, indexTem
|
|
|
84
100
|
headers,
|
|
85
101
|
});
|
|
86
102
|
}
|
|
103
|
+
logServerResponse('ssr', logger, request, response.status);
|
|
87
104
|
return response;
|
|
88
105
|
}
|
|
89
|
-
function isStreamableRequest(url) {
|
|
90
|
-
/**
|
|
91
|
-
* TODO: Add UA detection.
|
|
92
|
-
*/
|
|
93
|
-
const isBot = url.searchParams.has('_bot');
|
|
94
|
-
return !isBot;
|
|
95
|
-
}
|
|
96
106
|
/**
|
|
97
107
|
* Generate the contents of the `head` tag, and update the existing `<title>` tag
|
|
98
108
|
* if one exists, and if a title is passed.
|
|
@@ -117,3 +127,57 @@ function generateHeadTag(head) {
|
|
|
117
127
|
return `<head>${headHtml}</head>`;
|
|
118
128
|
};
|
|
119
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Determines if the request is from a bot, using the URL and User Agent
|
|
132
|
+
*/
|
|
133
|
+
function isBotUA(url, userAgent) {
|
|
134
|
+
return (url.searchParams.has('_bot') || (!!userAgent && botUARegex.test(userAgent)));
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* An alphabetized list of User Agents of known bots, combined from lists found at:
|
|
138
|
+
* https://github.com/vercel/next.js/blob/d87dc2b5a0b3fdbc0f6806a47be72bad59564bd0/packages/next/server/utils.ts#L18-L22
|
|
139
|
+
* https://github.com/GoogleChrome/rendertron/blob/6f681688737846b28754fbfdf5db173846a826df/middleware/src/middleware.ts#L24-L41
|
|
140
|
+
*/
|
|
141
|
+
const botUserAgents = [
|
|
142
|
+
'AdsBot-Google',
|
|
143
|
+
'applebot',
|
|
144
|
+
'Baiduspider',
|
|
145
|
+
'baiduspider',
|
|
146
|
+
'bingbot',
|
|
147
|
+
'Bingbot',
|
|
148
|
+
'BingPreview',
|
|
149
|
+
'bitlybot',
|
|
150
|
+
'Discordbot',
|
|
151
|
+
'DuckDuckBot',
|
|
152
|
+
'Embedly',
|
|
153
|
+
'facebookcatalog',
|
|
154
|
+
'facebookexternalhit',
|
|
155
|
+
'Google-PageRenderer',
|
|
156
|
+
'Googlebot',
|
|
157
|
+
'googleweblight',
|
|
158
|
+
'ia_archive',
|
|
159
|
+
'LinkedInBot',
|
|
160
|
+
'Mediapartners-Google',
|
|
161
|
+
'outbrain',
|
|
162
|
+
'pinterest',
|
|
163
|
+
'quora link preview',
|
|
164
|
+
'redditbot',
|
|
165
|
+
'rogerbot',
|
|
166
|
+
'showyoubot',
|
|
167
|
+
'SkypeUriPreview',
|
|
168
|
+
'Slackbot',
|
|
169
|
+
'Slurp',
|
|
170
|
+
'sogou',
|
|
171
|
+
'Storebot-Google',
|
|
172
|
+
'TelegramBot',
|
|
173
|
+
'tumblr',
|
|
174
|
+
'Twitterbot',
|
|
175
|
+
'vkShare',
|
|
176
|
+
'W3C_Validator',
|
|
177
|
+
'WhatsApp',
|
|
178
|
+
'yandex',
|
|
179
|
+
];
|
|
180
|
+
/**
|
|
181
|
+
* Creates a regex based on the botUserAgents array
|
|
182
|
+
*/
|
|
183
|
+
const botUARegex = new RegExp(botUserAgents.join('|'), 'i');
|
|
@@ -6,9 +6,7 @@ export interface UseShopQueryResponse<T> {
|
|
|
6
6
|
errors: any;
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
|
-
* The `useShopQuery` hook allows you to make server-only GraphQL queries to the Storefront API.
|
|
10
|
-
* \> Note:
|
|
11
|
-
* \> It must be a descendent of a `ShopifyProvider` component.
|
|
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.
|
|
12
10
|
*/
|
|
13
11
|
export declare function useShopQuery<T>({ query, variables, cache, }: {
|
|
14
12
|
/** A string of the GraphQL query.
|