@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.
Files changed (59) hide show
  1. package/dist/esnext/components/CartLineQuantityAdjustButton/CartLineQuantityAdjustButton.js +4 -0
  2. package/dist/esnext/entry-server.js +70 -37
  3. package/dist/esnext/foundation/Router/DefaultRoutes.d.ts +3 -1
  4. package/dist/esnext/foundation/Router/DefaultRoutes.js +2 -2
  5. package/dist/esnext/foundation/ServerStateProvider/ServerStateProvider.client.js +4 -2
  6. package/dist/esnext/foundation/useQuery/hooks.js +4 -3
  7. package/dist/esnext/foundation/useShop/use-shop.d.ts +1 -1
  8. package/dist/esnext/foundation/useShop/use-shop.js +1 -1
  9. package/dist/esnext/framework/Hydration/ServerComponentRequest.server.d.ts +1 -0
  10. package/dist/esnext/framework/Hydration/ServerComponentRequest.server.js +2 -0
  11. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-config.d.ts +1 -1
  12. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-config.js +2 -1
  13. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-middleware.d.ts +7 -1
  14. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-middleware.js +15 -1
  15. package/dist/esnext/handle-event.js +74 -10
  16. package/dist/esnext/hooks/useShopQuery/hooks.d.ts +1 -3
  17. package/dist/esnext/hooks/useShopQuery/hooks.js +6 -5
  18. package/dist/esnext/index.d.ts +1 -1
  19. package/dist/esnext/index.js +1 -1
  20. package/dist/esnext/types.d.ts +4 -0
  21. package/dist/esnext/utilities/index.d.ts +2 -0
  22. package/dist/esnext/utilities/index.js +2 -0
  23. package/dist/esnext/utilities/log/index.d.ts +1 -0
  24. package/dist/esnext/utilities/log/index.js +1 -0
  25. package/dist/esnext/utilities/log/log.d.ts +20 -0
  26. package/dist/esnext/utilities/log/log.js +71 -0
  27. package/dist/esnext/utilities/timing.d.ts +7 -0
  28. package/dist/esnext/utilities/timing.js +14 -0
  29. package/dist/esnext/version.d.ts +1 -1
  30. package/dist/esnext/version.js +1 -1
  31. package/dist/node/framework/Hydration/ServerComponentRequest.server.d.ts +1 -0
  32. package/dist/node/framework/Hydration/ServerComponentRequest.server.js +2 -0
  33. package/dist/node/framework/plugins/vite-plugin-hydrogen-config.d.ts +1 -1
  34. package/dist/node/framework/plugins/vite-plugin-hydrogen-config.js +2 -1
  35. package/dist/node/framework/plugins/vite-plugin-hydrogen-middleware.d.ts +7 -1
  36. package/dist/node/framework/plugins/vite-plugin-hydrogen-middleware.js +15 -1
  37. package/dist/node/handle-event.js +74 -10
  38. package/dist/node/types.d.ts +4 -0
  39. package/dist/node/utilities/index.d.ts +2 -0
  40. package/dist/node/utilities/index.js +9 -1
  41. package/dist/node/utilities/log/index.d.ts +1 -0
  42. package/dist/node/utilities/log/index.js +9 -0
  43. package/dist/node/utilities/log/log.d.ts +20 -0
  44. package/dist/node/utilities/log/log.js +78 -0
  45. package/dist/node/utilities/timing.d.ts +7 -0
  46. package/dist/node/utilities/timing.js +18 -0
  47. package/dist/node/version.d.ts +1 -1
  48. package/dist/node/version.js +1 -1
  49. package/dist/worker/framework/Hydration/ServerComponentRequest.server.d.ts +1 -0
  50. package/dist/worker/framework/Hydration/ServerComponentRequest.server.js +2 -0
  51. package/dist/worker/handle-event.js +74 -10
  52. package/dist/worker/types.d.ts +4 -0
  53. package/dist/worker/utilities/log/index.d.ts +1 -0
  54. package/dist/worker/utilities/log/index.js +1 -0
  55. package/dist/worker/utilities/log/log.d.ts +20 -0
  56. package/dist/worker/utilities/log/log.js +71 -0
  57. package/dist/worker/utilities/timing.d.ts +7 -0
  58. package/dist/worker/utilities/timing.js +14 -0
  59. 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 { renderToReadableStream, pipeToNodeWritable,
2
+ import {
3
3
  // @ts-ignore
4
- } from 'react-dom/unstable-fizz';
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
- console.error('Fatal', error);
80
+ log.fatal(error);
75
81
  });
76
82
  let didError;
77
83
  const head = template.match(/<head>(.+?)<\/head>/s)[1];
78
- const { startWriting, abort } = pipeToNodeWritable(React.createElement(Html, { head: head },
79
- React.createElement(ReactApp, { ...state })), response, {
80
- onReadyToStream() {
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, startWriting, dev ? didError : undefined);
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, startWriting, dev ? didError : undefined);
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
- console.error(error);
130
+ log.error(error);
124
131
  },
125
132
  });
126
- setTimeout(abort, STREAM_ABORT_TIMEOUT_MS);
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
- console.error('Fatal', error);
156
+ log.fatal(error);
142
157
  });
143
158
  let didError;
144
159
  const writer = new HydrationWriter();
145
- const { startWriting, abort } = pipeToNodeWritable(React.createElement(HydrationContext.Provider, { value: true },
146
- React.createElement(ReactApp, { ...state })), writer, {
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
- startWriting();
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
- console.error(error);
179
+ log.error(error);
163
180
  },
164
181
  });
165
- setTimeout(abort, STREAM_ABORT_TIMEOUT_MS);
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 renderCache = {};
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, request: request, response: componentResponse })))));
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
- console.error(error);
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 { startWriting } = pipeToNodeWritable(app, writer, {
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
- startWriting();
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
- console.error(error);
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, startWriting, error) {
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
- startWriting();
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
- if ('request' in newValue || 'response' in newValue) {
29
- console.warn(`Custom "request" and "response" properties in server state are ignored. Use a different name.`);
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
- console.log('[useQuery] cache stale; generating new response in background');
37
+ log.debug('[useQuery] cache stale; generating new response in background');
37
38
  const lockKey = `lock-${key}`;
38
39
  runDelayedFunction(async () => {
39
- console.log(`[stale regen] fetching cache lock`);
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
- console.error(`Error generating async response: ${e.message}`);
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`.The `useShop` hook provides access to values within `shopify.config.js`. It must be a descendent of a `ShopifyProvider` component.
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`.The `useShop` hook provides access to values within `shopify.config.js`. It must be a descendent of a `ShopifyProvider` component.
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);
@@ -7,6 +7,7 @@
7
7
  */
8
8
  export declare class ServerComponentRequest extends Request {
9
9
  cookies: Map<string, string>;
10
+ time: number;
10
11
  constructor(input: any);
11
12
  constructor(input: RequestInfo, init?: RequestInit);
12
13
  private parseCookies;
@@ -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,3 +1,3 @@
1
- import type { Plugin } from 'vite';
1
+ import { Plugin } from 'vite';
2
2
  declare const _default: () => Plugin;
3
3
  export default _default;
@@ -1,7 +1,7 @@
1
1
  export default () => {
2
2
  return {
3
3
  name: 'vite-plugin-hydrogen-config',
4
- config: (_, env) => ({
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 type { Plugin } from 'vite';
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 isStreamable = streamableResponse && isStreamableRequest(url);
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, { context: {}, request, response: streamableResponse, dev });
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, { request, context: {}, isReactHydrationRequest, dev });
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.