@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.
Files changed (87) hide show
  1. package/dist/esnext/components/CartLineQuantityAdjustButton/CartLineQuantityAdjustButton.js +4 -0
  2. package/dist/esnext/components/ProductProvider/ProductProvider.client.d.ts +3 -2
  3. package/dist/esnext/components/ShopPayButton/ShopPayButton.client.js +1 -1
  4. package/dist/esnext/entry-server.js +76 -36
  5. package/dist/esnext/foundation/RenderCacheProvider/hook.js +1 -1
  6. package/dist/esnext/foundation/RenderCacheProvider/types.d.ts +9 -2
  7. package/dist/esnext/foundation/Router/DefaultRoutes.d.ts +4 -2
  8. package/dist/esnext/foundation/Router/DefaultRoutes.js +8 -4
  9. package/dist/esnext/foundation/ServerStateProvider/ServerStateProvider.client.js +4 -2
  10. package/dist/esnext/foundation/useQuery/hooks.js +6 -4
  11. package/dist/esnext/foundation/useShop/use-shop.d.ts +1 -1
  12. package/dist/esnext/foundation/useShop/use-shop.js +1 -1
  13. package/dist/esnext/framework/Hydration/ServerComponentRequest.server.d.ts +1 -0
  14. package/dist/esnext/framework/Hydration/ServerComponentRequest.server.js +5 -0
  15. package/dist/esnext/framework/middleware.js +20 -22
  16. package/dist/esnext/framework/plugin.d.ts +1 -1
  17. package/dist/esnext/framework/plugin.js +3 -1
  18. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-config.d.ts +1 -1
  19. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-config.js +2 -1
  20. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-middleware.d.ts +7 -1
  21. package/dist/esnext/framework/plugins/vite-plugin-hydrogen-middleware.js +17 -1
  22. package/dist/esnext/framework/plugins/vite-plugin-purge-query-cache.d.ts +3 -0
  23. package/dist/esnext/framework/plugins/vite-plugin-purge-query-cache.js +11 -0
  24. package/dist/esnext/handle-event.js +80 -12
  25. package/dist/esnext/hooks/useShopQuery/hooks.d.ts +4 -4
  26. package/dist/esnext/hooks/useShopQuery/hooks.js +32 -11
  27. package/dist/esnext/index.d.ts +2 -1
  28. package/dist/esnext/index.js +2 -1
  29. package/dist/esnext/types.d.ts +9 -4
  30. package/dist/esnext/utilities/apiRoutes.d.ts +21 -0
  31. package/dist/esnext/utilities/apiRoutes.js +73 -0
  32. package/dist/esnext/utilities/fetch.js +3 -6
  33. package/dist/esnext/utilities/flattenConnection/flattenConnection.d.ts +1 -1
  34. package/dist/esnext/utilities/flattenConnection/flattenConnection.js +1 -1
  35. package/dist/esnext/utilities/index.d.ts +1 -0
  36. package/dist/esnext/utilities/index.js +1 -0
  37. package/dist/esnext/utilities/log/index.d.ts +1 -0
  38. package/dist/esnext/utilities/log/index.js +1 -0
  39. package/dist/esnext/utilities/log/log.d.ts +17 -0
  40. package/dist/esnext/utilities/log/log.js +76 -0
  41. package/dist/esnext/utilities/matchPath.d.ts +10 -0
  42. package/dist/esnext/utilities/matchPath.js +54 -0
  43. package/dist/esnext/utilities/timing.d.ts +7 -0
  44. package/dist/esnext/utilities/timing.js +14 -0
  45. package/dist/esnext/version.d.ts +1 -1
  46. package/dist/esnext/version.js +1 -1
  47. package/dist/node/framework/Hydration/ServerComponentRequest.server.d.ts +1 -0
  48. package/dist/node/framework/Hydration/ServerComponentRequest.server.js +5 -0
  49. package/dist/node/framework/middleware.js +20 -22
  50. package/dist/node/framework/plugin.d.ts +1 -1
  51. package/dist/node/framework/plugin.js +3 -1
  52. package/dist/node/framework/plugins/vite-plugin-hydrogen-config.d.ts +1 -1
  53. package/dist/node/framework/plugins/vite-plugin-hydrogen-config.js +2 -1
  54. package/dist/node/framework/plugins/vite-plugin-hydrogen-middleware.d.ts +7 -1
  55. package/dist/node/framework/plugins/vite-plugin-hydrogen-middleware.js +17 -1
  56. package/dist/node/framework/plugins/vite-plugin-purge-query-cache.d.ts +3 -0
  57. package/dist/node/framework/plugins/vite-plugin-purge-query-cache.js +16 -0
  58. package/dist/node/handle-event.js +80 -12
  59. package/dist/node/types.d.ts +9 -4
  60. package/dist/node/utilities/apiRoutes.d.ts +21 -0
  61. package/dist/node/utilities/apiRoutes.js +79 -0
  62. package/dist/node/utilities/fetch.js +3 -6
  63. package/dist/node/utilities/flattenConnection/flattenConnection.d.ts +1 -1
  64. package/dist/node/utilities/flattenConnection/flattenConnection.js +1 -1
  65. package/dist/node/utilities/index.d.ts +1 -0
  66. package/dist/node/utilities/index.js +3 -1
  67. package/dist/node/utilities/log/log.d.ts +17 -0
  68. package/dist/node/utilities/log/log.js +83 -0
  69. package/dist/node/utilities/matchPath.d.ts +10 -0
  70. package/dist/node/utilities/matchPath.js +58 -0
  71. package/dist/node/utilities/timing.d.ts +7 -0
  72. package/dist/node/utilities/timing.js +18 -0
  73. package/dist/node/version.d.ts +1 -1
  74. package/dist/node/version.js +1 -1
  75. package/dist/worker/framework/Hydration/ServerComponentRequest.server.d.ts +1 -0
  76. package/dist/worker/framework/Hydration/ServerComponentRequest.server.js +5 -0
  77. package/dist/worker/handle-event.js +80 -12
  78. package/dist/worker/types.d.ts +9 -4
  79. package/dist/worker/utilities/apiRoutes.d.ts +21 -0
  80. package/dist/worker/utilities/apiRoutes.js +73 -0
  81. package/dist/worker/utilities/log/log.d.ts +17 -0
  82. package/dist/worker/utilities/log/log.js +76 -0
  83. package/dist/worker/utilities/matchPath.d.ts +10 -0
  84. package/dist/worker/utilities/matchPath.js +54 -0
  85. package/dist/worker/utilities/timing.d.ts +7 -0
  86. package/dist/worker/utilities/timing.js +14 -0
  87. 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: string;
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 = atob(gid).split('/').pop();
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 { 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, 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
- console.error('Fatal', error);
86
+ log.fatal(error);
75
87
  });
76
88
  let didError;
77
89
  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() {
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, startWriting, dev ? didError : undefined);
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, startWriting, dev ? didError : undefined);
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
- console.error(error);
136
+ log.error(error);
124
137
  },
125
138
  });
126
- setTimeout(abort, STREAM_ABORT_TIMEOUT_MS);
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
- console.error('Fatal', error);
160
+ log.fatal(error);
142
161
  });
143
162
  let didError;
144
163
  const writer = new HydrationWriter();
145
- const { startWriting, abort } = pipeToNodeWritable(React.createElement(HydrationContext.Provider, { value: true },
146
- React.createElement(ReactApp, { ...state })), writer, {
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
- startWriting();
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
- console.error(error);
183
+ log.error(error);
163
184
  },
164
185
  });
165
- setTimeout(abort, STREAM_ABORT_TIMEOUT_MS);
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 renderCache = {};
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, request: request, response: componentResponse })))));
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
- console.error(error);
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 { startWriting } = pipeToNodeWritable(app, writer, {
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
- startWriting();
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
- console.error(error);
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, startWriting, error) {
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
- startWriting();
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 = { data: e }));
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 interface RenderCacheResult<T> {
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
- export declare type ImportGlobEagerOutput = Record<string, Record<'default', any>>;
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).map((key) => {
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
- 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/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
- return useRenderCacheData(key, cachedQueryFnBuilder(key, queryFn, queryOptions));
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
- console.log('[useQuery] cache stale; generating new response in background');
38
+ log.debug('[useQuery] cache stale; generating new response in background');
37
39
  const lockKey = `lock-${key}`;
38
40
  runDelayedFunction(async () => {
39
- console.log(`[stale regen] fetching cache lock`);
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
- console.error(`Error generating async response: ${e.message}`);
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`.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
@@ -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('node-fetch');
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.fetch = fetch.default;
30
+ globalThis.Request = Request;
39
31
  // @ts-ignore
40
- globalThis.Request = fetch.Request;
32
+ globalThis.Response = Response;
41
33
  // @ts-ignore
42
- globalThis.Response = fetch.Response;
34
+ globalThis.Headers = Headers;
43
35
  // @ts-ignore
44
- globalThis.Headers = fetch.Headers;
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
- response.end(eventResponse.body);
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: HydrogenVitePluginOptions) => ("" | import("vite").Plugin | import("vite").PluginOption[] | undefined)[];
2
+ declare const _default: (shopifyConfig: ShopifyConfig, pluginOptions?: HydrogenVitePluginOptions) => (false | "" | import("vite").Plugin | import("vite").PluginOption[] | undefined)[];
3
3
  export default _default;
@@ -1,14 +1,16 @@
1
1
  import hydrogenConfig from './plugins/vite-plugin-hydrogen-config';
2
2
  import hydrogenMiddleware from './plugins/vite-plugin-hydrogen-middleware';
3
3
  import reactServerComponentShim from './plugins/vite-plugin-react-server-components-shim';
4
+ import purgeQueryCache from './plugins/vite-plugin-purge-query-cache';
4
5
  import inspect from 'vite-plugin-inspect';
5
6
  import react from '@vitejs/plugin-react';
6
- export default (shopifyConfig, pluginOptions) => {
7
+ export default (shopifyConfig, pluginOptions = {}) => {
7
8
  return [
8
9
  process.env.VITE_INSPECT && inspect(),
9
10
  hydrogenConfig(),
10
11
  hydrogenMiddleware(shopifyConfig, pluginOptions),
11
12
  reactServerComponentShim(),
12
13
  react(),
14
+ pluginOptions.purgeQueryCacheOnBuild && purgeQueryCache(),
13
15
  ];
14
16
  };
@@ -1,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;