@shopify/hydrogen 1.0.2 → 1.1.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 (33) hide show
  1. package/dist/esnext/entry-server.js +10 -9
  2. package/dist/esnext/foundation/Analytics/connectors/PerformanceMetrics/PerformanceMetrics.client.js +4 -2
  3. package/dist/esnext/foundation/DevTools/components/Heading.js +1 -1
  4. package/dist/esnext/foundation/DevTools/components/Panels.js +25 -19
  5. package/dist/esnext/foundation/DevTools/components/Performance.client.js +0 -1
  6. package/dist/esnext/foundation/DevTools/components/Settings.client.js +1 -4
  7. package/dist/esnext/foundation/DevTools/components/Table.js +3 -3
  8. package/dist/esnext/foundation/HydrogenRequest/HydrogenRequest.server.js +1 -3
  9. package/dist/esnext/foundation/ServerPropsProvider/ServerPropsProvider.js +3 -3
  10. package/dist/esnext/foundation/fetchSync/ResponseSync.d.ts +14 -0
  11. package/dist/esnext/foundation/fetchSync/ResponseSync.js +38 -0
  12. package/dist/esnext/foundation/fetchSync/client/fetchSync.d.ts +2 -2
  13. package/dist/esnext/foundation/fetchSync/client/fetchSync.js +4 -9
  14. package/dist/esnext/foundation/fetchSync/server/fetchSync.d.ts +2 -2
  15. package/dist/esnext/foundation/fetchSync/server/fetchSync.js +5 -11
  16. package/dist/esnext/framework/graphiql.js +26 -30
  17. package/dist/esnext/framework/plugins/vite-plugin-platform-entry.js +35 -6
  18. package/dist/esnext/hooks/useShopQuery/hooks.js +9 -6
  19. package/dist/esnext/platforms/index.d.ts +1 -0
  20. package/dist/esnext/platforms/index.js +1 -0
  21. package/dist/esnext/platforms/node.js +2 -8
  22. package/dist/esnext/platforms/virtual.d.ts +8 -0
  23. package/dist/esnext/platforms/virtual.js +14 -0
  24. package/dist/esnext/platforms/worker.js +7 -7
  25. package/dist/esnext/version.d.ts +1 -1
  26. package/dist/esnext/version.js +1 -1
  27. package/dist/node/framework/graphiql.js +26 -30
  28. package/dist/node/framework/plugins/vite-plugin-platform-entry.js +35 -6
  29. package/package.json +6 -1
  30. package/vendor/react-server-dom-vite/cjs/react-server-dom-vite-plugin.js +26 -12
  31. package/vendor/react-server-dom-vite/esm/react-server-dom-vite-plugin.js +26 -12
  32. package/dist/esnext/foundation/fetchSync/types.d.ts +0 -5
  33. package/dist/esnext/foundation/fetchSync/types.js +0 -1
@@ -10,7 +10,6 @@ import { ServerPropsProvider } from './foundation/ServerPropsProvider';
10
10
  import { isBotUA } from './utilities/bot-ua';
11
11
  import { getCache, setCache } from './foundation/runtime';
12
12
  import { ssrRenderToPipeableStream, ssrRenderToReadableStream, rscRenderToReadableStream, createFromReadableStream, bufferReadableStream, } from './streaming.server';
13
- import { RSC_PATHNAME } from './constants';
14
13
  import { stripScriptsFromTemplate } from './utilities/template';
15
14
  import { setLogger } from './utilities/log/log';
16
15
  import { Analytics } from './foundation/Analytics/Analytics.server';
@@ -110,7 +109,7 @@ export const renderHydrogen = (App) => {
110
109
  async function processRequest(handleRequest, App, url, request, sessionApi, options, response, hydrogenConfig, revalidate = false) {
111
110
  const { dev, nonce, indexTemplate, streamableResponse: nodeResponse } = options;
112
111
  const log = getLoggerWithContext(request);
113
- const isRSCRequest = url.pathname === RSC_PATHNAME;
112
+ const isRSCRequest = request.isRscRequest();
114
113
  const apiRoute = !isRSCRequest && getApiRoute(url, hydrogenConfig.routes);
115
114
  // The API Route might have a default export, making it also a server component
116
115
  // If it does, only render the API route if the request method is GET
@@ -123,8 +122,11 @@ async function processRequest(handleRequest, App, url, request, sessionApi, opti
123
122
  : apiResponse;
124
123
  }
125
124
  const state = isRSCRequest
126
- ? parseJSON(url.searchParams.get('state') || '{}')
127
- : { pathname: url.pathname, search: url.search };
125
+ ? parseJSON(decodeURIComponent(url.searchParams.get('state') || '{}'))
126
+ : {
127
+ pathname: decodeURIComponent(url.pathname),
128
+ search: decodeURIComponent(url.search),
129
+ };
128
130
  const rsc = runRSC({ App, state, log, request, response });
129
131
  if (isRSCRequest) {
130
132
  const buffered = await bufferReadableStream(rsc.readable.getReader());
@@ -552,13 +554,13 @@ function tagOnWrite(response) {
552
554
  async function cacheResponse(response, request, chunks, revalidate) {
553
555
  const cache = getCache();
554
556
  /**
555
- * Only cache on cachable responses where response
557
+ * Only full page cache on cachable responses where response
556
558
  *
557
559
  * - have content to cache
558
560
  * - have status 200
559
561
  * - does not have no-store on cache-control header
560
562
  * - does not have set-cookie header
561
- * - is not a POST request
563
+ * - is a GET request
562
564
  * - does not have a session or does not have an active customer access token
563
565
  */
564
566
  if (cache &&
@@ -566,7 +568,7 @@ async function cacheResponse(response, request, chunks, revalidate) {
566
568
  response.status === 200 &&
567
569
  response.cache().mode !== NO_STORE &&
568
570
  !response.headers.has('Set-Cookie') &&
569
- !/post/i.test(request.method) &&
571
+ /get/i.test(request.method) &&
570
572
  !sessionHasCustomerAccessToken(request)) {
571
573
  if (revalidate) {
572
574
  await saveCacheResponse(response, request, chunks);
@@ -593,10 +595,9 @@ async function saveCacheResponse(response, request, chunks) {
593
595
  const cache = getCache();
594
596
  if (cache && chunks.length > 0) {
595
597
  const { headers, status, statusText } = getResponseOptions(response);
596
- const url = new URL(request.url);
597
598
  headers.set('cache-control', response.cacheControlHeader);
598
599
  const currentHeader = headers.get('Content-Type');
599
- if (!currentHeader && url.pathname !== RSC_PATHNAME) {
600
+ if (!currentHeader && !request.isRscRequest()) {
600
601
  headers.set('Content-Type', HTML_CONTENT_TYPE);
601
602
  }
602
603
  await setItemInCache(request.cacheKey(), new Response(chunks.join(''), {
@@ -18,7 +18,7 @@ export function PerformanceMetrics() {
18
18
  const initTime = new Date().getTime();
19
19
  ClientAnalytics.publish(ClientAnalytics.eventNames.PERFORMANCE, true, data);
20
20
  const pageData = ClientAnalytics.getPageAnalyticsData();
21
- const shopId = pageData.shopify.shopId || '';
21
+ const shopId = pageData.shopify?.shopId;
22
22
  fetch('https://monorail-edge.shopifysvc.com/v1/produce', {
23
23
  method: 'post',
24
24
  headers: {
@@ -28,7 +28,9 @@ export function PerformanceMetrics() {
28
28
  schema_id: 'hydrogen_buyer_performance/2.0',
29
29
  payload: {
30
30
  ...data,
31
- shop_id: shopId.substring(shopId.lastIndexOf('/') + 1) || '',
31
+ shop_id: shopId
32
+ ? shopId.substring(shopId.lastIndexOf('/') + 1)
33
+ : '',
32
34
  },
33
35
  metadata: {
34
36
  event_created_at_ms: initTime,
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  export function Heading({ linkText, url, children, }) {
3
- return (React.createElement("span", { style: { display: 'flex', alignItems: 'baseline', padding: '0 0 0.5em' } },
3
+ return (React.createElement("span", { style: { display: 'flex', alignItems: 'baseline', padding: '0 0 0em' } },
4
4
  React.createElement("span", { style: { paddingRight: '0em', flex: 1, fontWeight: 'bold' } },
5
5
  children,
6
6
  ' '),
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
2
2
  import { ClientAnalytics } from '../../Analytics';
3
3
  import { Performance } from './Performance.client';
4
4
  import { Settings } from './Settings.client';
5
+ const isComponentPanel = (panel) => panel.component !== undefined;
5
6
  export function Panels({ settings }) {
6
7
  const [selectedPanel, setSelectedPanel] = useState(0);
7
8
  const [navigations, setNavigations] = useState([]);
@@ -22,36 +23,41 @@ export function Panels({ settings }) {
22
23
  });
23
24
  }, [setNavigations, navigations]);
24
25
  const panels = getPanels({ settings, performance: { navigations } });
25
- const panelComponents = panels.map((obj, index) => (React.createElement("div", { key: obj.content, style: { display: selectedPanel === index ? 'block' : 'none' } }, obj.panel)));
26
+ const panelComponents = panels.map((obj, index) => isComponentPanel(obj) ? (React.createElement("div", { key: obj.content, style: { display: selectedPanel === index ? 'block' : 'none' } }, obj.component)) : null);
26
27
  return (React.createElement("div", { style: { display: 'flex', height: '100%' } },
27
- React.createElement("div", { style: { borderRight: '1px solid', padding: '1em 0em' } }, panels.map(({ content, icon, id }, index) => {
28
+ React.createElement("div", { style: { borderRight: '1px solid', padding: '1em 0em' } }, panels.map((panel, index) => {
28
29
  const active = selectedPanel === index;
29
- return (React.createElement("button", { key: id, type: "button", style: {
30
- lineHeight: 2,
31
- padding: '0em 1.25em',
32
- fontWeight: active ? 'bold' : 'normal',
33
- display: 'flex',
34
- alignItems: 'center',
35
- }, onClick: () => setSelectedPanel(index) },
36
- React.createElement("span", { style: { paddingRight: '0.4em' } }, icon),
37
- React.createElement("span", { style: { fontFamily: 'monospace' } }, content)));
30
+ const style = {
31
+ padding: '0em 1.25em',
32
+ fontWeight: 'bold',
33
+ textDecoration: active ? 'underline' : 'none',
34
+ display: 'flex',
35
+ justifyContent: 'space-between',
36
+ alignItems: 'center',
37
+ };
38
+ if (isComponentPanel(panel)) {
39
+ return (React.createElement("button", { key: panel.id, type: "button", style: style, onClick: () => setSelectedPanel(index) },
40
+ React.createElement("span", null, panel.content)));
41
+ }
42
+ return (React.createElement("a", { style: style, target: "_blank", rel: "noreferrer", href: panel.url, key: panel.url },
43
+ panel.content,
44
+ React.createElement("span", null, "\u2197")));
38
45
  })),
39
- React.createElement("div", { style: { padding: '1.25em', width: '100%' } }, panelComponents[selectedPanel ? selectedPanel : 0])));
40
- }
41
- function Panel({ children }) {
42
- return React.createElement("div", null, children);
46
+ React.createElement("div", { style: { padding: '1em', width: '100%' } }, panelComponents[selectedPanel ? selectedPanel : 0])));
43
47
  }
44
48
  function getPanels({ settings, performance }) {
45
49
  const panels = {
46
50
  settings: {
47
51
  content: 'Settings',
48
- panel: React.createElement(Settings, { ...settings }),
49
- icon: '🎛',
52
+ component: React.createElement(Settings, { ...settings }),
50
53
  },
51
54
  performance: {
52
55
  content: 'Performance',
53
- panel: React.createElement(Performance, { ...performance }),
54
- icon: '⏱',
56
+ component: React.createElement(Performance, { ...performance }),
57
+ },
58
+ graphiql: {
59
+ content: 'GraphiQL',
60
+ url: '/___graphql',
55
61
  },
56
62
  };
57
63
  return Object.keys(panels).map((key) => {
@@ -19,7 +19,6 @@ const Item = ({ label, value, unit }) => {
19
19
  return (React.createElement("span", { style: {
20
20
  fontFamily: 'monospace',
21
21
  padding: '0 2em 0 0',
22
- fontSize: '0.75em',
23
22
  } },
24
23
  label && label.padEnd(10),
25
24
  val));
@@ -1,5 +1,4 @@
1
1
  import React from 'react';
2
- import { Heading } from './Heading';
3
2
  import { Table } from './Table';
4
3
  const KEY_MAP = {
5
4
  locale: 'Locale',
@@ -14,7 +13,5 @@ export function Settings(props) {
14
13
  type: typeof value,
15
14
  };
16
15
  });
17
- return (React.createElement(React.Fragment, null,
18
- React.createElement(Heading, null, "Config"),
19
- React.createElement(Table, { items: items })));
16
+ return React.createElement(Table, { items: items });
20
17
  }
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  export function Table({ items }) {
3
- const itemsMarkup = items.map(({ key, value }) => (React.createElement("div", { key: key, style: { display: 'flex' } },
4
- React.createElement("span", { style: { width: '30%', fontFamily: 'monospace', paddingRight: '1em' } }, key),
5
- React.createElement("span", { style: { width: '70%', fontFamily: 'monospace', fontWeight: 'bold' } }, value))));
3
+ const itemsMarkup = items.map(({ key, value }) => (React.createElement("div", { key: key, style: { display: 'flex', paddingBottom: '1em', flexDirection: 'column' } },
4
+ React.createElement("span", { style: { fontWeight: 'bold' } }, key),
5
+ React.createElement("span", { style: { width: '70%', fontFamily: 'monospace' } }, value))));
6
6
  return React.createElement("ul", null, itemsMarkup);
7
7
  }
@@ -43,9 +43,7 @@ export class HydrogenRequest extends Request {
43
43
  }
44
44
  this.time = getTime();
45
45
  this.id = generateId();
46
- this.normalizedUrl = this.isRscRequest()
47
- ? normalizeUrl(this.url)
48
- : this.url;
46
+ this.normalizedUrl = decodeURIComponent(this.isRscRequest() ? normalizeUrl(this.url) : this.url);
49
47
  this.ctx = {
50
48
  cache: new Map(),
51
49
  head: new HeadData({}),
@@ -13,12 +13,12 @@ export function ServerPropsProvider({ initialServerProps, setServerPropsForRsc,
13
13
  setServerPropsForRsc((prev) => getNewValue(prev, input, propValue));
14
14
  });
15
15
  }, [setServerProps, setServerPropsForRsc]);
16
- const setLocationServerPropsCallback = useCallback((input, propValue) => {
16
+ const setLocationServerPropsCallback = useCallback((input) => {
17
17
  // Flush the existing user server state when location changes, leaving only the persisted state
18
18
  startTransition(() => {
19
- setServerPropsForRsc((prev) => getNewValue(prev, input, propValue));
19
+ setServerPropsForRsc(input);
20
20
  setServerProps({});
21
- setLocationServerProps((prev) => getNewValue(prev, input, propValue));
21
+ setLocationServerProps(input);
22
22
  });
23
23
  }, [setServerProps, setServerPropsForRsc, setLocationServerProps]);
24
24
  const getProposedLocationServerPropsCallback = useCallback((input, propValue) => {
@@ -0,0 +1,14 @@
1
+ declare type ResponseSyncInit = [string, ResponseInit];
2
+ export declare class ResponseSync extends Response {
3
+ #private;
4
+ bodyUsed: boolean;
5
+ constructor(init: ResponseSyncInit);
6
+ text(): string;
7
+ json(): any;
8
+ /**
9
+ * @deprecated Access response properties at the top level instead.
10
+ */
11
+ get response(): this;
12
+ static toSerializable(response: Response): Promise<ResponseSyncInit>;
13
+ }
14
+ export {};
@@ -0,0 +1,38 @@
1
+ import { parseJSON } from '../../utilities/parse';
2
+ import { log } from '../../utilities/log';
3
+ export class ResponseSync extends Response {
4
+ bodyUsed = true;
5
+ #text;
6
+ #json;
7
+ constructor(init) {
8
+ super(...init);
9
+ this.#text = init[0];
10
+ }
11
+ // @ts-expect-error Changing inherited types
12
+ text() {
13
+ return this.#text;
14
+ }
15
+ json() {
16
+ return (this.#json ??= parseJSON(this.#text));
17
+ }
18
+ /**
19
+ * @deprecated Access response properties at the top level instead.
20
+ */
21
+ get response() {
22
+ if (__HYDROGEN_DEV__) {
23
+ log.warn(`Property 'response' is deprecated from the result of 'fetchSync'.` +
24
+ ` Access response properties at the top level instead.`);
25
+ }
26
+ return this;
27
+ }
28
+ static async toSerializable(response) {
29
+ return [
30
+ await response.text(),
31
+ {
32
+ status: response.status,
33
+ statusText: response.statusText,
34
+ headers: Array.from(response.headers.entries()),
35
+ },
36
+ ];
37
+ }
38
+ }
@@ -1,8 +1,8 @@
1
- import type { FetchResponse } from '../types';
1
+ import { ResponseSync } from '../ResponseSync';
2
2
  /**
3
3
  * Fetch a URL for use in a client component Suspense boundary.
4
4
  */
5
- export declare function fetchSync(url: string, options?: RequestInit): FetchResponse;
5
+ export declare function fetchSync(url: string, options?: RequestInit): ResponseSync;
6
6
  /**
7
7
  * Preload a URL for use in a client component Suspense boundary.
8
8
  * Useful for placing higher in the tree to avoid waterfalls.
@@ -1,19 +1,14 @@
1
- import { parseJSON } from '../../../utilities/parse';
2
1
  import { suspendFunction, preloadFunction } from '../../../utilities/suspense';
2
+ import { ResponseSync } from '../ResponseSync';
3
3
  /**
4
4
  * Fetch a URL for use in a client component Suspense boundary.
5
5
  */
6
6
  export function fetchSync(url, options) {
7
- const [text, response] = suspendFunction([url, options], async () => {
7
+ const responseSyncInit = suspendFunction([url, options], async () => {
8
8
  const response = await globalThis.fetch(new URL(url, window.location.origin), options);
9
- const text = await response.text();
10
- return [text, response];
9
+ return ResponseSync.toSerializable(response);
11
10
  });
12
- return {
13
- response,
14
- json: () => parseJSON(text),
15
- text: () => text,
16
- };
11
+ return new ResponseSync(responseSyncInit);
17
12
  }
18
13
  /**
19
14
  * Preload a URL for use in a client component Suspense boundary.
@@ -1,8 +1,8 @@
1
1
  import { type HydrogenUseQueryOptions } from '../../useQuery/hooks';
2
- import type { FetchResponse } from '../types';
2
+ import { ResponseSync } from '../ResponseSync';
3
3
  /**
4
4
  * The `fetchSync` hook makes API requests and is the recommended way to make simple fetch calls on the server and the client.
5
5
  * It's designed similar to the [Web API's `fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch), only in a way
6
6
  * that supports [Suspense](https://reactjs.org/docs/concurrent-mode-suspense.html).
7
7
  */
8
- export declare function fetchSync(url: string, options?: Omit<RequestInit, 'cache'> & HydrogenUseQueryOptions): FetchResponse;
8
+ export declare function fetchSync(url: string, options?: Omit<RequestInit, 'cache'> & HydrogenUseQueryOptions): ResponseSync;
@@ -1,6 +1,6 @@
1
- import { parseJSON } from '../../../utilities/parse';
2
1
  import { useQuery } from '../../useQuery/hooks';
3
2
  import { useUrl } from '../../useUrl';
3
+ import { ResponseSync } from '../ResponseSync';
4
4
  /**
5
5
  * The `fetchSync` hook makes API requests and is the recommended way to make simple fetch calls on the server and the client.
6
6
  * It's designed similar to the [Web API's `fetch`](https://developer.mozilla.org/en-US/docs/Web/API/fetch), only in a way
@@ -10,11 +10,10 @@ export function fetchSync(url, options) {
10
10
  const { cache, preload, shouldCacheResponse, ...requestInit } = options ?? {};
11
11
  // eslint-disable-next-line react-hooks/rules-of-hooks
12
12
  const { origin } = useUrl();
13
- const { data: useQueryResponse, error } = useQuery(// eslint-disable-line react-hooks/rules-of-hooks
14
- [url, requestInit], async () => {
13
+ // eslint-disable-next-line react-hooks/rules-of-hooks
14
+ const { data, error } = useQuery([url, requestInit], async () => {
15
15
  const response = await globalThis.fetch(new URL(url, origin), requestInit);
16
- const text = await response.text();
17
- return [text, response];
16
+ return ResponseSync.toSerializable(response);
18
17
  }, {
19
18
  cache,
20
19
  preload,
@@ -23,10 +22,5 @@ export function fetchSync(url, options) {
23
22
  if (error) {
24
23
  throw error;
25
24
  }
26
- const [data, response] = useQueryResponse;
27
- return {
28
- response,
29
- json: () => parseJSON(data),
30
- text: () => data,
31
- };
25
+ return new ResponseSync(data);
32
26
  }
@@ -1,38 +1,34 @@
1
1
  export function graphiqlHtml(shop, token, apiVersion) {
2
- return `<html>
3
- <head>
4
- <title>Shopify Storefront API</title>
5
- <link href="https://unpkg.com/graphiql/graphiql.min.css" rel="stylesheet" />
6
- </head>
7
- <body style="margin: 0;">
8
- <div id="graphiql" style="height: 100vh;"></div>
9
- <script
10
- crossorigin
11
- src="https://unpkg.com/react/umd/react.production.min.js"
12
- ></script>
13
- <script
14
- crossorigin
15
- src="https://unpkg.com/react-dom/umd/react-dom.production.min.js"
16
- ></script>
17
- <script
18
- crossorigin
19
- src="https://unpkg.com/graphiql/graphiql.min.js"
20
- ></script>
21
- <script>
22
- const fetcher = GraphiQL.createFetcher({
23
- url: 'https://${shop}/api/${apiVersion}/graphql.json',
24
- headers: {
2
+ return `
3
+ <!DOCTYPE html>
4
+ <html>
5
+ <head>
6
+ <meta charset=utf-8/>
7
+ <meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal-ui">
8
+ <title>Shopify Storefront API</title>
9
+ <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/graphql-playground-react/build/static/css/index.css" />
10
+ <link rel="shortcut icon" href="//cdn.jsdelivr.net/npm/graphql-playground-react/build/favicon.png" />
11
+ <script src="//cdn.jsdelivr.net/npm/graphql-playground-react/build/static/js/middleware.js"></script>
12
+ </head>
13
+ <body>
14
+ <div id="root"></div>
15
+ <script>window.addEventListener('load', function (event) {
16
+ GraphQLPlayground.init(document.getElementById('root'), {
17
+ endpoint:'https://${shop}/api/${apiVersion}/graphql.json',
18
+ settings:{
19
+ 'request.globalHeaders': {
25
20
  Accept: 'application/json',
26
21
  'Content-Type': 'application/graphql',
27
22
  'X-Shopify-Storefront-Access-Token': '${token}'
28
23
  }
29
- });
30
- ReactDOM.render(
31
- React.createElement(GraphiQL, { fetcher: fetcher }),
32
- document.getElementById('graphiql'),
33
- );
34
- </script>
35
- </body>
24
+ },
25
+ tabs: [{
26
+ endpoint: 'https://${shop}/api/${apiVersion}/graphql.json',
27
+ query: '{ shop { name } }'
28
+ }]
29
+ })
30
+ })</script>
31
+ </body>
36
32
  </html>
37
33
  `;
38
34
  }
@@ -3,12 +3,16 @@ import { HYDROGEN_DEFAULT_SERVER_ENTRY } from './vite-plugin-hydrogen-middleware
3
3
  import MagicString from 'magic-string';
4
4
  import path from 'path';
5
5
  import fs from 'fs';
6
+ import fastGlob from 'fast-glob';
6
7
  const SSR_BUNDLE_NAME = 'index.js';
8
+ // Keep this in the outer scope to share it
9
+ // across client <> server builds.
10
+ let clientBuildPath;
7
11
  export default () => {
8
12
  let config;
9
13
  let isESM;
10
14
  return {
11
- name: 'vite-plugin-platform-entry',
15
+ name: 'hydrogen:platform-entry',
12
16
  enforce: 'pre',
13
17
  configResolved(_config) {
14
18
  config = _config;
@@ -29,18 +33,43 @@ export default () => {
29
33
  }
30
34
  return null;
31
35
  },
32
- transform(code, id) {
33
- if (normalizePath(id).includes('/hydrogen/dist/esnext/platforms/')) {
36
+ async transform(code, id, options) {
37
+ if (config.command === 'build' &&
38
+ options?.ssr &&
39
+ /@shopify\/hydrogen\/.+platforms\/virtual\./.test(normalizePath(id))) {
34
40
  const ms = new MagicString(code);
35
- ms.replace('__SERVER_ENTRY__', HYDROGEN_DEFAULT_SERVER_ENTRY);
36
- const indexTemplatePath = normalizePath(path.resolve(config.root, config.build.outDir, '..', 'client', 'index.html'));
37
- ms.replace('__INDEX_TEMPLATE__', indexTemplatePath);
41
+ ms.replace('__HYDROGEN_ENTRY__', HYDROGEN_DEFAULT_SERVER_ENTRY);
42
+ if (!clientBuildPath) {
43
+ // Default value
44
+ clientBuildPath = normalizePath(path.resolve(config.root, config.build.outDir, '..', 'client'));
45
+ }
46
+ ms.replace('__HYDROGEN_HTML_TEMPLATE__', normalizePath(path.resolve(clientBuildPath, 'index.html')));
47
+ ms.replace('__HYDROGEN_RELATIVE_CLIENT_BUILD__', normalizePath(path.relative(normalizePath(path.resolve(config.root, config.build.outDir)), clientBuildPath)));
48
+ const files = clientBuildPath
49
+ ? (await fastGlob('**/*', {
50
+ cwd: clientBuildPath,
51
+ ignore: ['**/index.html', `**/${config.build.assetsDir}/**`],
52
+ })).map((file) => '/' + file)
53
+ : [];
54
+ ms.replace("\\['__HYDROGEN_ASSETS__'\\]", JSON.stringify(files));
55
+ ms.replace('__HYDROGEN_ASSETS_DIR__', config.build.assetsDir);
56
+ ms.replace('__HYDROGEN_ASSETS_BASE_URL__', (process.env.HYDROGEN_ASSET_BASE_URL || '').replace(/\/$/, ''));
57
+ // Remove the poison pill
58
+ ms.replace('throw', '//');
38
59
  return {
39
60
  code: ms.toString(),
40
61
  map: ms.generateMap({ file: id, source: id }),
41
62
  };
42
63
  }
43
64
  },
65
+ buildEnd(err) {
66
+ if (!err && !config.build.ssr && config.command === 'build') {
67
+ // Save outDir from client build in the outer scope in order
68
+ // to read it during the server build. The CLI runs Vite in
69
+ // the same process so the scope is shared across builds.
70
+ clientBuildPath = normalizePath(path.resolve(config.root, config.build.outDir));
71
+ }
72
+ },
44
73
  generateBundle(options, bundle) {
45
74
  if (config.build.ssr) {
46
75
  const [key, value] = Object.entries(bundle).find(([, value]) => value.type === 'chunk' && value.isEntry);
@@ -40,15 +40,17 @@ export function useShopQuery({ query, variables = {}, cache, preload = false, })
40
40
  let text;
41
41
  let data;
42
42
  let useQueryError;
43
+ let response = null;
43
44
  try {
44
- text = fetchSync(url, {
45
+ response = fetchSync(url, {
45
46
  ...requestInit,
46
47
  cache,
47
48
  preload,
48
49
  shouldCacheResponse,
49
- }).text();
50
+ });
51
+ text = response.text();
50
52
  try {
51
- data = JSON.parse(text);
53
+ data = response.json();
52
54
  }
53
55
  catch (error) {
54
56
  useQueryError = new Error('Unable to parse response:\n' + text);
@@ -82,15 +84,16 @@ export function useShopQuery({ query, variables = {}, cache, preload = false, })
82
84
  */
83
85
  if (data?.errors) {
84
86
  const errors = Array.isArray(data.errors) ? data.errors : [data.errors];
87
+ const requestId = response?.headers?.get('x-request-id') ?? '';
85
88
  for (const error of errors) {
86
89
  if (__HYDROGEN_DEV__ && !__HYDROGEN_TEST__) {
87
- throw new Error(error.message);
90
+ throw new Error(`Storefront API GraphQL Error: ${error.message}.\nRequest id: ${requestId}`);
88
91
  }
89
92
  else {
90
- log.error('GraphQL Error', error);
93
+ log.error('Storefront API GraphQL Error', error, 'Storefront API GraphQL request id', requestId);
91
94
  }
92
95
  }
93
- log.error(`GraphQL errors: ${errors.length}`);
96
+ log.error(`Storefront API GraphQL error count: ${errors.length}`);
94
97
  }
95
98
  if (__HYDROGEN_DEV__ &&
96
99
  (log.options().showUnusedQueryProperties ||
@@ -0,0 +1 @@
1
+ export * from './virtual';
@@ -0,0 +1 @@
1
+ export * from './virtual';
@@ -1,11 +1,6 @@
1
1
  import '../utilities/web-api-polyfill';
2
2
  import path from 'path';
3
- // @ts-ignore
4
- // eslint-disable-next-line node/no-missing-import
5
- import entrypoint from '__SERVER_ENTRY__';
6
- // @ts-ignore
7
- // eslint-disable-next-line node/no-missing-import
8
- import indexTemplate from '__INDEX_TEMPLATE__?raw';
3
+ import { handleRequest, indexTemplate, relativeClientBuildPath } from './virtual';
9
4
  import { hydrogenMiddleware } from '../framework/middleware';
10
5
  // @ts-ignore
11
6
  import serveStatic from 'serve-static';
@@ -14,13 +9,12 @@ import compression from 'compression';
14
9
  import bodyParser from 'body-parser';
15
10
  import connect from 'connect';
16
11
  import { InMemoryCache } from '../framework/cache/in-memory';
17
- const handleRequest = entrypoint;
18
12
  export async function createServer({ cache = new InMemoryCache(), } = {}) {
19
13
  // @ts-ignore
20
14
  globalThis.Oxygen = { env: process.env };
21
15
  const app = connect();
22
16
  app.use(compression());
23
- app.use(serveStatic(path.resolve(__dirname, '../client'), {
17
+ app.use(serveStatic(path.resolve(__dirname, relativeClientBuildPath), {
24
18
  index: false,
25
19
  }));
26
20
  app.use(bodyParser.raw({ type: '*/*' }));
@@ -0,0 +1,8 @@
1
+ import type { RequestHandler } from '../shared-types';
2
+ export declare const handleRequest: RequestHandler;
3
+ export { default as indexTemplate } from '__HYDROGEN_HTML_TEMPLATE__?raw';
4
+ export declare const assets: Set<string>;
5
+ export declare const assetPrefix: string;
6
+ export declare const isAsset: (pathname?: string) => boolean;
7
+ export declare const relativeClientBuildPath: string;
8
+ export declare const assetBasePath: string;
@@ -0,0 +1,14 @@
1
+ // This file is modified by Vite at build time
2
+ // with user project information and re-exports it.
3
+ // @ts-ignore
4
+ // eslint-disable-next-line node/no-missing-import
5
+ import appEntry from '__HYDROGEN_ENTRY__';
6
+ export const handleRequest = appEntry;
7
+ // eslint-disable-next-line node/no-missing-import
8
+ export { default as indexTemplate } from '__HYDROGEN_HTML_TEMPLATE__?raw';
9
+ export const assets = new Set(['__HYDROGEN_ASSETS__']);
10
+ export const assetPrefix = '/__HYDROGEN_ASSETS_DIR__/';
11
+ export const isAsset = (pathname = '') => pathname.startsWith(assetPrefix) || assets.has(pathname);
12
+ export const relativeClientBuildPath = '__HYDROGEN_RELATIVE_CLIENT_BUILD__';
13
+ export const assetBasePath = '__HYDROGEN_ASSETS_BASE_URL__';
14
+ throw new Error('This file must be overwritten in a Vite plugin');
@@ -1,12 +1,12 @@
1
- // @ts-ignore
2
- // eslint-disable-next-line node/no-missing-import
3
- import entrypoint from '__SERVER_ENTRY__';
4
- // @ts-ignore
5
- // eslint-disable-next-line node/no-missing-import
6
- import indexTemplate from '__INDEX_TEMPLATE__?raw';
7
- const handleRequest = entrypoint;
1
+ import { handleRequest, indexTemplate, isAsset, assetBasePath } from './virtual';
8
2
  export default {
9
3
  async fetch(request, env, context) {
4
+ // Proxy assets to the CDN. This should be removed
5
+ // once the proxy is implemented in Oxygen itself.
6
+ const url = new URL(request.url);
7
+ if (assetBasePath && isAsset(url.pathname)) {
8
+ return fetch(request.url.replace(url.origin, assetBasePath), request);
9
+ }
10
10
  if (!globalThis.Oxygen) {
11
11
  globalThis.Oxygen = { env };
12
12
  }
@@ -1 +1 @@
1
- export declare const LIB_VERSION = "1.0.2";
1
+ export declare const LIB_VERSION = "1.1.0";
@@ -1 +1 @@
1
- export const LIB_VERSION = '1.0.2';
1
+ export const LIB_VERSION = '1.1.0';
@@ -2,40 +2,36 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.graphiqlHtml = void 0;
4
4
  function graphiqlHtml(shop, token, apiVersion) {
5
- return `<html>
6
- <head>
7
- <title>Shopify Storefront API</title>
8
- <link href="https://unpkg.com/graphiql/graphiql.min.css" rel="stylesheet" />
9
- </head>
10
- <body style="margin: 0;">
11
- <div id="graphiql" style="height: 100vh;"></div>
12
- <script
13
- crossorigin
14
- src="https://unpkg.com/react/umd/react.production.min.js"
15
- ></script>
16
- <script
17
- crossorigin
18
- src="https://unpkg.com/react-dom/umd/react-dom.production.min.js"
19
- ></script>
20
- <script
21
- crossorigin
22
- src="https://unpkg.com/graphiql/graphiql.min.js"
23
- ></script>
24
- <script>
25
- const fetcher = GraphiQL.createFetcher({
26
- url: 'https://${shop}/api/${apiVersion}/graphql.json',
27
- headers: {
5
+ return `
6
+ <!DOCTYPE html>
7
+ <html>
8
+ <head>
9
+ <meta charset=utf-8/>
10
+ <meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal-ui">
11
+ <title>Shopify Storefront API</title>
12
+ <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/graphql-playground-react/build/static/css/index.css" />
13
+ <link rel="shortcut icon" href="//cdn.jsdelivr.net/npm/graphql-playground-react/build/favicon.png" />
14
+ <script src="//cdn.jsdelivr.net/npm/graphql-playground-react/build/static/js/middleware.js"></script>
15
+ </head>
16
+ <body>
17
+ <div id="root"></div>
18
+ <script>window.addEventListener('load', function (event) {
19
+ GraphQLPlayground.init(document.getElementById('root'), {
20
+ endpoint:'https://${shop}/api/${apiVersion}/graphql.json',
21
+ settings:{
22
+ 'request.globalHeaders': {
28
23
  Accept: 'application/json',
29
24
  'Content-Type': 'application/graphql',
30
25
  'X-Shopify-Storefront-Access-Token': '${token}'
31
26
  }
32
- });
33
- ReactDOM.render(
34
- React.createElement(GraphiQL, { fetcher: fetcher }),
35
- document.getElementById('graphiql'),
36
- );
37
- </script>
38
- </body>
27
+ },
28
+ tabs: [{
29
+ endpoint: 'https://${shop}/api/${apiVersion}/graphql.json',
30
+ query: '{ shop { name } }'
31
+ }]
32
+ })
33
+ })</script>
34
+ </body>
39
35
  </html>
40
36
  `;
41
37
  }
@@ -8,12 +8,16 @@ const vite_plugin_hydrogen_middleware_1 = require("./vite-plugin-hydrogen-middle
8
8
  const magic_string_1 = __importDefault(require("magic-string"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const fs_1 = __importDefault(require("fs"));
11
+ const fast_glob_1 = __importDefault(require("fast-glob"));
11
12
  const SSR_BUNDLE_NAME = 'index.js';
13
+ // Keep this in the outer scope to share it
14
+ // across client <> server builds.
15
+ let clientBuildPath;
12
16
  exports.default = () => {
13
17
  let config;
14
18
  let isESM;
15
19
  return {
16
- name: 'vite-plugin-platform-entry',
20
+ name: 'hydrogen:platform-entry',
17
21
  enforce: 'pre',
18
22
  configResolved(_config) {
19
23
  config = _config;
@@ -34,18 +38,43 @@ exports.default = () => {
34
38
  }
35
39
  return null;
36
40
  },
37
- transform(code, id) {
38
- if ((0, vite_1.normalizePath)(id).includes('/hydrogen/dist/esnext/platforms/')) {
41
+ async transform(code, id, options) {
42
+ if (config.command === 'build' &&
43
+ options?.ssr &&
44
+ /@shopify\/hydrogen\/.+platforms\/virtual\./.test((0, vite_1.normalizePath)(id))) {
39
45
  const ms = new magic_string_1.default(code);
40
- ms.replace('__SERVER_ENTRY__', vite_plugin_hydrogen_middleware_1.HYDROGEN_DEFAULT_SERVER_ENTRY);
41
- const indexTemplatePath = (0, vite_1.normalizePath)(path_1.default.resolve(config.root, config.build.outDir, '..', 'client', 'index.html'));
42
- ms.replace('__INDEX_TEMPLATE__', indexTemplatePath);
46
+ ms.replace('__HYDROGEN_ENTRY__', vite_plugin_hydrogen_middleware_1.HYDROGEN_DEFAULT_SERVER_ENTRY);
47
+ if (!clientBuildPath) {
48
+ // Default value
49
+ clientBuildPath = (0, vite_1.normalizePath)(path_1.default.resolve(config.root, config.build.outDir, '..', 'client'));
50
+ }
51
+ ms.replace('__HYDROGEN_HTML_TEMPLATE__', (0, vite_1.normalizePath)(path_1.default.resolve(clientBuildPath, 'index.html')));
52
+ ms.replace('__HYDROGEN_RELATIVE_CLIENT_BUILD__', (0, vite_1.normalizePath)(path_1.default.relative((0, vite_1.normalizePath)(path_1.default.resolve(config.root, config.build.outDir)), clientBuildPath)));
53
+ const files = clientBuildPath
54
+ ? (await (0, fast_glob_1.default)('**/*', {
55
+ cwd: clientBuildPath,
56
+ ignore: ['**/index.html', `**/${config.build.assetsDir}/**`],
57
+ })).map((file) => '/' + file)
58
+ : [];
59
+ ms.replace("\\['__HYDROGEN_ASSETS__'\\]", JSON.stringify(files));
60
+ ms.replace('__HYDROGEN_ASSETS_DIR__', config.build.assetsDir);
61
+ ms.replace('__HYDROGEN_ASSETS_BASE_URL__', (process.env.HYDROGEN_ASSET_BASE_URL || '').replace(/\/$/, ''));
62
+ // Remove the poison pill
63
+ ms.replace('throw', '//');
43
64
  return {
44
65
  code: ms.toString(),
45
66
  map: ms.generateMap({ file: id, source: id }),
46
67
  };
47
68
  }
48
69
  },
70
+ buildEnd(err) {
71
+ if (!err && !config.build.ssr && config.command === 'build') {
72
+ // Save outDir from client build in the outer scope in order
73
+ // to read it during the server build. The CLI runs Vite in
74
+ // the same process so the scope is shared across builds.
75
+ clientBuildPath = (0, vite_1.normalizePath)(path_1.default.resolve(config.root, config.build.outDir));
76
+ }
77
+ },
49
78
  generateBundle(options, bundle) {
50
79
  if (config.build.ssr) {
51
80
  const [key, value] = Object.entries(bundle).find(([, value]) => value.type === 'chunk' && value.isEntry);
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "engines": {
8
8
  "node": ">=14"
9
9
  },
10
- "version": "1.0.2",
10
+ "version": "1.1.0",
11
11
  "description": "Modern custom Shopify storefronts",
12
12
  "license": "MIT",
13
13
  "main": "dist/esnext/index.js",
@@ -35,6 +35,11 @@
35
35
  "import": "./dist/esnext/framework/cache/*.js",
36
36
  "require": "./dist/node/framework/cache/*.js"
37
37
  },
38
+ "./platforms": {
39
+ "types": "./dist/esnext/platforms/virtual.d.ts",
40
+ "import": "./dist/esnext/platforms/virtual.js",
41
+ "require": "./dist/node/platforms/virtual.js"
42
+ },
38
43
  "./package.json": "./package.json",
39
44
  "./*": "./dist/esnext/*.js"
40
45
  },
@@ -297,6 +297,19 @@ function ReactFlightVitePlugin() {
297
297
 
298
298
  return findClientBoundariesForClientBuild(serverBuildEntries, optimizeBoundaries !== false).then(injectGlobs);
299
299
  }
300
+ },
301
+ handleHotUpdate: function (_ref2) {
302
+ var modules = _ref2.modules;
303
+
304
+ if (modules.some(function (mod) {
305
+ return mod.meta && mod.meta.isClientComponent;
306
+ })) {
307
+ return modules.filter(function (mod) {
308
+ return !mod.meta || !mod.meta.ssr;
309
+ });
310
+ }
311
+
312
+ return modules;
300
313
  }
301
314
  };
302
315
  }
@@ -466,9 +479,9 @@ function isDirectImportInServer(originalMod, currentMod, accModInfo) {
466
479
  exports: []
467
480
  };
468
481
  lastModImports.forEach(function (mod) {
469
- mod.variables.forEach(function (_ref2) {
470
- var name = _ref2[0],
471
- alias = _ref2[1];
482
+ mod.variables.forEach(function (_ref3) {
483
+ var name = _ref3[0],
484
+ alias = _ref3[1];
472
485
 
473
486
  if (name === '*' && !alias) {
474
487
  var _accModInfo$exports;
@@ -501,8 +514,8 @@ function isDirectImportInServer(originalMod, currentMod, accModInfo) {
501
514
  // the original module before marking it as client boundary.
502
515
 
503
516
  return currentMod.meta.imports.some(function (imp) {
504
- return imp.from === accModInfo.file && (imp.variables || []).some(function (_ref3) {
505
- var name = _ref3[0];
517
+ return imp.from === accModInfo.file && (imp.variables || []).some(function (_ref4) {
518
+ var name = _ref4[0];
506
519
  return accModInfo.exports.includes(name);
507
520
  });
508
521
  });
@@ -540,12 +553,12 @@ function augmentModuleGraph(moduleGraph, id, code, root, resolveAlias) {
540
553
 
541
554
 
542
555
  var imports = [];
543
- rawImports.forEach(function (_ref4) {
544
- var startMod = _ref4.s,
545
- endMod = _ref4.e,
546
- dynamicImportIndex = _ref4.d,
547
- startStatement = _ref4.ss,
548
- endStatement = _ref4.se;
556
+ rawImports.forEach(function (_ref5) {
557
+ var startMod = _ref5.s,
558
+ endMod = _ref5.e,
559
+ dynamicImportIndex = _ref5.d,
560
+ startStatement = _ref5.ss,
561
+ endStatement = _ref5.se;
549
562
  if (dynamicImportIndex !== -1) return; // Skip dynamic imports for now
550
563
 
551
564
  var rawModPath = code.slice(startMod, endMod);
@@ -593,7 +606,8 @@ function augmentModuleGraph(moduleGraph, id, code, root, resolveAlias) {
593
606
  assign(currentModule.meta, {
594
607
  isFacade: isFacade,
595
608
  namedExports: namedExports,
596
- imports: imports
609
+ imports: imports,
610
+ ssr: true
597
611
  });
598
612
  }
599
613
 
@@ -293,6 +293,19 @@ function ReactFlightVitePlugin() {
293
293
 
294
294
  return findClientBoundariesForClientBuild(serverBuildEntries, optimizeBoundaries !== false).then(injectGlobs);
295
295
  }
296
+ },
297
+ handleHotUpdate: function (_ref2) {
298
+ var modules = _ref2.modules;
299
+
300
+ if (modules.some(function (mod) {
301
+ return mod.meta && mod.meta.isClientComponent;
302
+ })) {
303
+ return modules.filter(function (mod) {
304
+ return !mod.meta || !mod.meta.ssr;
305
+ });
306
+ }
307
+
308
+ return modules;
296
309
  }
297
310
  };
298
311
  }
@@ -462,9 +475,9 @@ function isDirectImportInServer(originalMod, currentMod, accModInfo) {
462
475
  exports: []
463
476
  };
464
477
  lastModImports.forEach(function (mod) {
465
- mod.variables.forEach(function (_ref2) {
466
- var name = _ref2[0],
467
- alias = _ref2[1];
478
+ mod.variables.forEach(function (_ref3) {
479
+ var name = _ref3[0],
480
+ alias = _ref3[1];
468
481
 
469
482
  if (name === '*' && !alias) {
470
483
  var _accModInfo$exports;
@@ -497,8 +510,8 @@ function isDirectImportInServer(originalMod, currentMod, accModInfo) {
497
510
  // the original module before marking it as client boundary.
498
511
 
499
512
  return currentMod.meta.imports.some(function (imp) {
500
- return imp.from === accModInfo.file && (imp.variables || []).some(function (_ref3) {
501
- var name = _ref3[0];
513
+ return imp.from === accModInfo.file && (imp.variables || []).some(function (_ref4) {
514
+ var name = _ref4[0];
502
515
  return accModInfo.exports.includes(name);
503
516
  });
504
517
  });
@@ -536,12 +549,12 @@ function augmentModuleGraph(moduleGraph, id, code, root, resolveAlias) {
536
549
 
537
550
 
538
551
  var imports = [];
539
- rawImports.forEach(function (_ref4) {
540
- var startMod = _ref4.s,
541
- endMod = _ref4.e,
542
- dynamicImportIndex = _ref4.d,
543
- startStatement = _ref4.ss,
544
- endStatement = _ref4.se;
552
+ rawImports.forEach(function (_ref5) {
553
+ var startMod = _ref5.s,
554
+ endMod = _ref5.e,
555
+ dynamicImportIndex = _ref5.d,
556
+ startStatement = _ref5.ss,
557
+ endStatement = _ref5.se;
545
558
  if (dynamicImportIndex !== -1) return; // Skip dynamic imports for now
546
559
 
547
560
  var rawModPath = code.slice(startMod, endMod);
@@ -589,7 +602,8 @@ function augmentModuleGraph(moduleGraph, id, code, root, resolveAlias) {
589
602
  assign(currentModule.meta, {
590
603
  isFacade: isFacade,
591
604
  namedExports: namedExports,
592
- imports: imports
605
+ imports: imports,
606
+ ssr: true
593
607
  });
594
608
  }
595
609
 
@@ -1,5 +0,0 @@
1
- export interface FetchResponse {
2
- response: Response;
3
- json: () => any;
4
- text: () => any;
5
- }
@@ -1 +0,0 @@
1
- export {};