@shopify/cli-hydrogen 5.3.1 → 5.4.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.
@@ -1,8 +1,8 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import Command from '@shopify/cli-kit/node/base-command';
3
3
  import { outputInfo, outputWarn, outputContent, outputToken } from '@shopify/cli-kit/node/output';
4
- import { rmdir, fileSize, fileExists, copyFile } from '@shopify/cli-kit/node/fs';
5
- import { resolvePath, relativePath } from '@shopify/cli-kit/node/path';
4
+ import { rmdir, fileSize, glob, readFile, writeFile, fileExists, copyFile } from '@shopify/cli-kit/node/fs';
5
+ import { resolvePath, joinPath, relativePath } from '@shopify/cli-kit/node/path';
6
6
  import { getPackageManager } from '@shopify/cli-kit/node/node-package-manager';
7
7
  import colors from '@shopify/cli-kit/node/colors';
8
8
  import { getProjectPaths, getRemixConfig, handleRemixImportFail, assertOxygenChecks } from '../../lib/remix-config.js';
@@ -13,6 +13,7 @@ import { muteRemixLogs, createRemixLogger } from '../../lib/log.js';
13
13
  import { codegen } from '../../lib/codegen.js';
14
14
  import { hasMetafile, buildBundleAnalysis, getBundleAnalysisSummary } from '../../lib/bundle/analyzer.js';
15
15
  import { AbortError } from '@shopify/cli-kit/node/error';
16
+ import { isCI } from '../../lib/is-ci.js';
16
17
 
17
18
  const LOG_WORKER_BUILT = "\u{1F4E6} Worker built";
18
19
  const MAX_WORKER_BUNDLE_SIZE = 10;
@@ -26,21 +27,27 @@ class Build extends Command {
26
27
  allowNo: true,
27
28
  default: true
28
29
  }),
29
- ["bundle-stats"]: Flags.boolean({
30
+ "bundle-stats": Flags.boolean({
30
31
  description: "Show a bundle size summary after building.",
31
32
  default: true,
32
33
  allowNo: true
33
34
  }),
35
+ "lockfile-check": Flags.boolean({
36
+ description: "Checks that there is exactly 1 valid lockfile in the project.",
37
+ env: "SHOPIFY_HYDROGEN_FLAG_LOCKFILE_CHECK",
38
+ default: true,
39
+ allowNo: true
40
+ }),
34
41
  "disable-route-warning": Flags.boolean({
35
42
  description: "Disable warning about missing standard routes.",
36
43
  env: "SHOPIFY_HYDROGEN_FLAG_DISABLE_ROUTE_WARNING"
37
44
  }),
38
- ["codegen-unstable"]: Flags.boolean({
45
+ "codegen-unstable": Flags.boolean({
39
46
  description: "Generate types for the Storefront API queries found in your project.",
40
47
  required: false,
41
48
  default: false
42
49
  }),
43
- ["codegen-config-path"]: commonFlags.codegenConfigPath,
50
+ "codegen-config-path": commonFlags.codegenConfigPath,
44
51
  base: deprecated("--base")(),
45
52
  entry: deprecated("--entry")(),
46
53
  target: deprecated("--target")()
@@ -62,6 +69,7 @@ async function runBuild({
62
69
  sourcemap = false,
63
70
  disableRouteWarning = false,
64
71
  bundleStats = true,
72
+ lockfileCheck = true,
65
73
  assetPath
66
74
  }) {
67
75
  if (!process.env.NODE_ENV) {
@@ -71,7 +79,10 @@ async function runBuild({
71
79
  process.env.HYDROGEN_ASSET_BASE_URL = assetPath;
72
80
  }
73
81
  const { root, buildPath, buildPathClient, buildPathWorkerFile, publicPath } = getProjectPaths(directory);
74
- await Promise.all([checkLockfileStatus(root), muteRemixLogs()]);
82
+ if (lockfileCheck) {
83
+ await checkLockfileStatus(root, isCI());
84
+ }
85
+ await muteRemixLogs();
75
86
  console.time(LOG_WORKER_BUILT);
76
87
  outputInfo(`
77
88
  \u{1F3D7}\uFE0F Building in ${process.env.NODE_ENV} mode...`);
@@ -135,10 +146,25 @@ This build is missing ${missingRoutes.length} route${missingRoutes.length > 1 ?
135
146
  );
136
147
  }
137
148
  }
149
+ if (process.env.NODE_ENV !== "development") {
150
+ await cleanClientSourcemaps(buildPathClient);
151
+ }
138
152
  if (!process.env.SHOPIFY_UNIT_TEST && !assetPath) {
139
153
  process.exit(0);
140
154
  }
141
155
  }
156
+ async function cleanClientSourcemaps(buildPathClient) {
157
+ const bundleFiles = await glob(joinPath(buildPathClient, "**/*.js"));
158
+ await Promise.all(
159
+ bundleFiles.map(async (filePath) => {
160
+ const file = await readFile(filePath);
161
+ return await writeFile(
162
+ filePath,
163
+ file.replace(/\/\/# sourceMappingURL=.+\.js\.map$/gm, "")
164
+ );
165
+ })
166
+ );
167
+ }
142
168
  async function writeBundleAnalysis(buildPath, root, buildPathWorkerFile, sizeMB, bundleStats, remixConfig) {
143
169
  const bundleAnalysisPath = await buildBundleAnalysis(buildPath);
144
170
  outputInfo(
@@ -18,6 +18,7 @@ import { getAllEnvironmentVariables } from '../../lib/environment-variables.js';
18
18
  import { getConfig } from '../../lib/shopify-config.js';
19
19
  import { setupLiveReload } from '../../lib/live-reload.js';
20
20
  import { checkRemixVersions } from '../../lib/remix-version-check.js';
21
+ import { getGraphiQLUrl } from '../../lib/graphiql-url.js';
21
22
 
22
23
  const LOG_REBUILDING = "\u{1F9F1} Rebuilding...";
23
24
  const LOG_REBUILT = "\u{1F680} Rebuilt";
@@ -121,16 +122,19 @@ async function runDev({
121
122
  },
122
123
  workerRuntime
123
124
  );
124
- const graphiqlUrl = `${miniOxygen.listeningAt}/graphiql`;
125
125
  const debugNetworkUrl = `${miniOxygen.listeningAt}/debug-network`;
126
- enhanceH2Logs({ graphiqlUrl, ...remixConfig });
126
+ enhanceH2Logs({ host: miniOxygen.listeningAt, ...remixConfig });
127
127
  miniOxygen.showBanner({
128
128
  appName: storefront ? colors.cyan(storefront?.title) : void 0,
129
129
  headlinePrefix: initialBuildDurationMs > 0 ? `Initial build: ${initialBuildDurationMs}ms
130
130
  ` : "",
131
131
  extraLines: [
132
- colors.dim(`
133
- View GraphiQL API browser: ${graphiqlUrl}`),
132
+ colors.dim(
133
+ `
134
+ View GraphiQL API browser: ${getGraphiQLUrl({
135
+ host: miniOxygen.listeningAt
136
+ })}`
137
+ ),
134
138
  workerRuntime ? "" : colors.dim(
135
139
  `
136
140
  View server-side network requests: ${debugNetworkUrl}`
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * A side bar component with Overlay that works without JavaScript.
3
3
  * @example
4
- * ```ts
5
- * <Aside id="search-aside" heading="SEARCH">`
4
+ * ```jsx
5
+ * <Aside id="search-aside" heading="SEARCH">
6
6
  * <input type="search" />
7
7
  * ...
8
8
  * </Aside>
@@ -450,13 +450,11 @@ function usePredictiveSearch(): UseSearchReturn {
450
450
 
451
451
  /**
452
452
  * Converts a plural search type to a singular search type
453
- * @param type - The plural search type
454
- * @returns The singular search type
455
453
  *
456
454
  * @example
457
- * ```ts
458
- * pluralToSingularSearchType('articles') // => 'ARTICLE'
459
- * pluralToSingularSearchType(['articles', 'products']) // => 'ARTICLE,PRODUCT'
455
+ * ```js
456
+ * pluralToSingularSearchType('articles'); // => 'ARTICLE'
457
+ * pluralToSingularSearchType(['articles', 'products']); // => 'ARTICLE,PRODUCT'
460
458
  * ```
461
459
  */
462
460
  function pluralToSingularSearchType(
@@ -189,14 +189,13 @@ export function CatchBoundary() {
189
189
  * @see https://shopify.dev/docs/api/storefront/latest/objects/CustomerAccessToken
190
190
  *
191
191
  * @example
192
- * ```ts
193
- * //
192
+ * ```js
194
193
  * const {isLoggedIn, headers} = await validateCustomerAccessToken(
195
194
  * customerAccessToken,
196
195
  * session,
197
- * );
198
- * ```
199
- * */
196
+ * );
197
+ * ```
198
+ */
200
199
  async function validateCustomerAccessToken(
201
200
  session: HydrogenSession,
202
201
  customerAccessToken?: CustomerAccessToken,
@@ -40,7 +40,8 @@ function FeaturedCollection({
40
40
  }: {
41
41
  collection: FeaturedCollectionFragment;
42
42
  }) {
43
- const image = collection.image;
43
+ if (!collection) return null;
44
+ const image = collection?.image;
44
45
  return (
45
46
  <Link
46
47
  className="featured-collection"
@@ -108,8 +108,6 @@ async function fetchPredictiveSearchResults({
108
108
 
109
109
  /**
110
110
  * Normalize results and apply tracking qurery parameters to each result url
111
- * @param predictiveSearch
112
- * @param locale
113
111
  */
114
112
  export function normalizePredictiveSearchResults(
115
113
  predictiveSearch: PredictiveSearchQuery['predictiveSearch'],
@@ -3,21 +3,20 @@ import {redirect, type LoaderArgs} from '@shopify/remix-oxygen';
3
3
  /**
4
4
  * Automatically creates a new cart based on the URL and redirects straight to checkout.
5
5
  * Expected URL structure:
6
- * ```ts
6
+ * ```js
7
7
  * /cart/<variant_id>:<quantity>
8
8
  *
9
9
  * ```
10
+ *
10
11
  * More than one `<variant_id>:<quantity>` separated by a comma, can be supplied in the URL, for
11
12
  * carts with more than one product variant.
12
13
  *
13
- * @param `?discount` an optional discount code to apply to the cart
14
14
  * @example
15
- * Example path creating a cart with two product variants, different quantities, and a discount code:
16
- * ```ts
15
+ * Example path creating a cart with two product variants, different quantities, and a discount code in the querystring:
16
+ * ```js
17
17
  * /cart/41007289663544:1,41007289696312:2?discount=HYDROBOARD
18
18
  *
19
19
  * ```
20
- * @preserve
21
20
  */
22
21
  export async function loader({request, context, params}: LoaderArgs) {
23
22
  const {cart} = context;
@@ -66,7 +66,7 @@ function CollectionItem({
66
66
  to={`/collections/${collection.handle}`}
67
67
  prefetch="intent"
68
68
  >
69
- {collection.image && (
69
+ {collection?.image && (
70
70
  <Image
71
71
  alt={collection.image.altText || collection.title}
72
72
  aspectRatio="1/1"
@@ -3,14 +3,13 @@ import {redirect, type LoaderArgs} from '@shopify/remix-oxygen';
3
3
  /**
4
4
  * Automatically applies a discount found on the url
5
5
  * If a cart exists it's updated with the discount, otherwise a cart is created with the discount already applied
6
- * @param ?redirect an optional path to return to otherwise return to the home page
6
+ *
7
7
  * @example
8
- * Example path applying a discount and redirecting
9
- * ```ts
8
+ * Example path applying a discount and optional redirecting (defaults to the home page)
9
+ * ```js
10
10
  * /discount/FREESHIPPING?redirect=/products
11
11
  *
12
12
  * ```
13
- * @preserve
14
13
  */
15
14
  export async function loader({request, context, params}: LoaderArgs) {
16
15
  const {cart} = context;
@@ -15,8 +15,8 @@
15
15
  "dependencies": {
16
16
  "@remix-run/react": "1.19.1",
17
17
  "@shopify/cli": "3.49.2",
18
- "@shopify/cli-hydrogen": "^5.3.1",
19
- "@shopify/hydrogen": "^2023.7.8",
18
+ "@shopify/cli-hydrogen": "^5.4.0",
19
+ "@shopify/hydrogen": "^2023.7.9",
20
20
  "@shopify/remix-oxygen": "^1.1.4",
21
21
  "graphql": "^16.6.0",
22
22
  "graphql-tag": "^2.12.6",
@@ -30,7 +30,7 @@
30
30
  "@shopify/prettier-config": "^1.1.2",
31
31
  "@total-typescript/ts-reset": "^0.4.2",
32
32
  "@types/eslint": "^8.4.10",
33
- "@types/react": "^18.2.20",
33
+ "@types/react": "^18.2.22",
34
34
  "@types/react-dom": "^18.2.7",
35
35
  "eslint": "^8.20.0",
36
36
  "eslint-plugin-hydrogen": "0.12.2",
@@ -2,24 +2,28 @@ import { fileExists } from '@shopify/cli-kit/node/fs';
2
2
  import { resolvePath } from '@shopify/cli-kit/node/path';
3
3
  import { checkIfIgnoredInGitRepository } from '@shopify/cli-kit/node/git';
4
4
  import { renderWarning } from '@shopify/cli-kit/node/ui';
5
+ import { AbortError } from '@shopify/cli-kit/node/error';
5
6
  import { lockfiles } from '@shopify/cli-kit/node/node-package-manager';
6
7
 
7
- function missingLockfileWarning() {
8
- renderWarning({
9
- headline: "No lockfile found",
10
- body: `If you don\u2019t commit a lockfile, then your app might install the wrong package versions when deploying. To avoid versioning issues, generate a new lockfile and commit it to your repository.`,
11
- nextSteps: [
12
- [
13
- "Generate a lockfile. Run",
14
- {
15
- command: "npm|yarn|pnpm install"
16
- }
17
- ],
18
- "Commit the new file to your repository"
19
- ]
20
- });
8
+ function missingLockfileWarning(shouldExit) {
9
+ const headline = "No lockfile found";
10
+ const body = `If you don\u2019t commit a lockfile, then your app might install the wrong package versions when deploying. To avoid versioning issues, generate a new lockfile and commit it to your repository.`;
11
+ const nextSteps = [
12
+ [
13
+ "Generate a lockfile. Run",
14
+ {
15
+ command: "npm|yarn|pnpm install"
16
+ }
17
+ ],
18
+ "Commit the new file to your repository"
19
+ ];
20
+ if (shouldExit) {
21
+ throw new AbortError(headline, body, nextSteps);
22
+ } else {
23
+ renderWarning({ headline, body, nextSteps });
24
+ }
21
25
  }
22
- function multipleLockfilesWarning(lockfiles2) {
26
+ function multipleLockfilesWarning(lockfiles2, shouldExit) {
23
27
  const packageManagers = {
24
28
  "yarn.lock": "yarn",
25
29
  "package-lock.json": "npm",
@@ -28,30 +32,32 @@ function multipleLockfilesWarning(lockfiles2) {
28
32
  const lockfileList = lockfiles2.map((lockfile) => {
29
33
  return `${lockfile} (created by ${packageManagers[lockfile]})`;
30
34
  });
31
- renderWarning({
32
- headline: "Multiple lockfiles found",
33
- body: [
34
- `Your project contains more than one lockfile. This can cause version conflicts when installing and deploying your app. The following lockfiles were detected:
35
+ const headline = "Multiple lockfiles found";
36
+ const body = [
37
+ `Your project contains more than one lockfile. This can cause version conflicts when installing and deploying your app. The following lockfiles were detected:
35
38
  `,
36
- { list: { items: lockfileList } }
37
- ],
38
- nextSteps: [
39
- "Delete any unneeded lockfiles",
40
- "Commit the change to your repository"
41
- ]
42
- });
39
+ { list: { items: lockfileList } }
40
+ ];
41
+ const nextSteps = [
42
+ "Delete any unneeded lockfiles",
43
+ "Commit the change to your repository"
44
+ ];
45
+ if (shouldExit) {
46
+ throw new AbortError(headline, body, nextSteps);
47
+ } else {
48
+ renderWarning({ headline, body, nextSteps });
49
+ }
43
50
  }
44
51
  function lockfileIgnoredWarning(lockfile) {
45
- renderWarning({
46
- headline: "Lockfile ignored by Git",
47
- body: `Your project\u2019s lockfile isn\u2019t being tracked by Git. If you don\u2019t commit a lockfile, then your app might install the wrong package versions when deploying.`,
48
- nextSteps: [
49
- `In your project\u2019s .gitignore file, delete any references to ${lockfile}`,
50
- "Commit the change to your repository"
51
- ]
52
- });
52
+ const headline = "Lockfile ignored by Git";
53
+ const body = `Your project\u2019s lockfile isn\u2019t being tracked by Git. If you don\u2019t commit a lockfile, then your app might install the wrong package versions when deploying.`;
54
+ const nextSteps = [
55
+ `In your project\u2019s .gitignore file, delete any references to ${lockfile}`,
56
+ "Commit the change to your repository"
57
+ ];
58
+ renderWarning({ headline, body, nextSteps });
53
59
  }
54
- async function checkLockfileStatus(directory) {
60
+ async function checkLockfileStatus(directory, shouldExit = false) {
55
61
  if (process.env.LOCAL_DEV)
56
62
  return;
57
63
  const availableLockfiles = [];
@@ -60,21 +66,20 @@ async function checkLockfileStatus(directory) {
60
66
  availableLockfiles.push(lockFileName);
61
67
  }
62
68
  }
63
- if (!availableLockfiles.length) {
64
- return missingLockfileWarning();
69
+ if (availableLockfiles.length === 0) {
70
+ return missingLockfileWarning(shouldExit);
65
71
  }
66
72
  if (availableLockfiles.length > 1) {
67
- return multipleLockfilesWarning(availableLockfiles);
73
+ return multipleLockfilesWarning(availableLockfiles, shouldExit);
68
74
  }
69
- try {
70
- const lockfile = availableLockfiles[0];
71
- const ignoredLockfile = await checkIfIgnoredInGitRepository(directory, [
72
- lockfile
73
- ]);
74
- if (ignoredLockfile.length) {
75
- lockfileIgnoredWarning(lockfile);
76
- }
77
- } catch {
75
+ const lockfile = availableLockfiles[0];
76
+ const ignoredLockfile = await checkIfIgnoredInGitRepository(directory, [
77
+ lockfile
78
+ ]).catch(() => {
79
+ return [];
80
+ });
81
+ if (ignoredLockfile.length > 0) {
82
+ lockfileIgnoredWarning(lockfile);
78
83
  }
79
84
  }
80
85
 
@@ -53,6 +53,15 @@ describe("checkLockfileStatus()", () => {
53
53
  );
54
54
  });
55
55
  });
56
+ it("throws when shouldExit is true", async () => {
57
+ await inTemporaryDirectory(async (tmpDir) => {
58
+ await writeFile(joinPath(tmpDir, "package-lock.json"), "");
59
+ await writeFile(joinPath(tmpDir, "pnpm-lock.yaml"), "");
60
+ await expect(checkLockfileStatus(tmpDir, true)).rejects.toThrow(
61
+ /Multiple lockfiles found/is
62
+ );
63
+ });
64
+ });
56
65
  });
57
66
  describe("when a lockfile is missing", () => {
58
67
  it("renders a warning", async () => {
@@ -61,5 +70,12 @@ describe("checkLockfileStatus()", () => {
61
70
  expect(outputMock.warn()).toMatch(/ warning .+ No lockfile found .+/is);
62
71
  });
63
72
  });
73
+ it("throws when shouldExit is true", async () => {
74
+ await inTemporaryDirectory(async (tmpDir) => {
75
+ await expect(checkLockfileStatus(tmpDir, true)).rejects.toThrow(
76
+ /No lockfile found/is
77
+ );
78
+ });
79
+ });
64
80
  });
65
81
  });
@@ -0,0 +1,15 @@
1
+ function getGraphiQLUrl({
2
+ host = "",
3
+ graphql
4
+ }) {
5
+ let url = `${host.endsWith("/") ? host.slice(0, -1) : host}/graphiql`;
6
+ if (graphql) {
7
+ let { query, variables } = graphql;
8
+ if (typeof variables !== "string")
9
+ variables = JSON.stringify(variables);
10
+ url += `?query=${encodeURIComponent(query)}${variables ? `&variables=${encodeURIComponent(variables)}` : ""}`;
11
+ }
12
+ return url;
13
+ }
14
+
15
+ export { getGraphiQLUrl };
@@ -0,0 +1,6 @@
1
+ function isCI() {
2
+ const { env } = process;
3
+ return env.CI === "false" ? false : !!(env.CI || env.CI_NAME || env.BUILD_NUMBER || env.TF_BUILD);
4
+ }
5
+
6
+ export { isCI };
package/dist/lib/log.js CHANGED
@@ -2,6 +2,7 @@ import { renderFatalError, renderWarning, renderInfo } from '@shopify/cli-kit/no
2
2
  import { BugError } from '@shopify/cli-kit/node/error';
3
3
  import { outputContent, outputToken } from '@shopify/cli-kit/node/output';
4
4
  import colors from '@shopify/cli-kit/node/colors';
5
+ import { getGraphiQLUrl } from './graphiql-url.js';
5
6
 
6
7
  const originalConsole = { ...console };
7
8
  const methodsReplaced = /* @__PURE__ */ new Set();
@@ -194,11 +195,11 @@ function enhanceH2Logs(options) {
194
195
  }
195
196
  }
196
197
  if (typeof cause !== "string" && !!cause?.graphql?.query) {
197
- const { query, variables } = cause.graphql;
198
- const link = `${options.graphiqlUrl}?query=${encodeURIComponent(
199
- query
200
- )}${variables ? `&variables=${encodeURIComponent(variables)}` : ""}`;
201
- const [, queryType, queryName] = query.match(/(query|mutation)\s+(\w+)/) || [];
198
+ const link = getGraphiQLUrl({
199
+ host: options.host,
200
+ graphql: cause.graphql
201
+ });
202
+ const [, queryType, queryName] = cause.graphql.query.match(/(query|mutation)\s+(\w+)/) || [];
202
203
  tryMessage = (tryMessage ? `${tryMessage}
203
204
 
204
205
  ` : "") + outputContent`To debug the ${queryType || "query"}${queryName ? ` \`${colors.whiteBright(queryName)}\`` : ""}, try it in ${outputToken.link(colors.bold("GraphiQL"), link)}.`.value;
@@ -5,7 +5,7 @@ import { resetAllLogs, enhanceH2Logs } from './log.js';
5
5
 
6
6
  describe("log replacer", () => {
7
7
  describe("enhanceH2Logs", () => {
8
- const graphiqlUrl = "http://localhost:3000/graphiql";
8
+ const host = "http://localhost:3000";
9
9
  const rootDirectory = fileURLToPath(import.meta.url);
10
10
  const outputMock = mockAndCaptureOutput();
11
11
  beforeEach(() => {
@@ -18,7 +18,7 @@ describe("log replacer", () => {
18
18
  });
19
19
  describe("enhances h2:info pattern", () => {
20
20
  it("renders in an info banner", () => {
21
- enhanceH2Logs({ graphiqlUrl, rootDirectory });
21
+ enhanceH2Logs({ host, rootDirectory });
22
22
  console.warn("[h2:info:storefront.query] Tip");
23
23
  const message = outputMock.info();
24
24
  expect(message).not.toMatch("h2");
@@ -29,7 +29,7 @@ describe("log replacer", () => {
29
29
  });
30
30
  describe("enhances h2:warn pattern", () => {
31
31
  it("renders in a warning banner", () => {
32
- enhanceH2Logs({ graphiqlUrl, rootDirectory });
32
+ enhanceH2Logs({ host, rootDirectory });
33
33
  console.warn("[h2:warn:storefront.query] Wrong query 1");
34
34
  const warning = outputMock.warn();
35
35
  expect(warning).not.toMatch("h2");
@@ -38,7 +38,7 @@ describe("log replacer", () => {
38
38
  expect(warning).toMatch("Wrong query");
39
39
  });
40
40
  it("shows links from the last line as a list", () => {
41
- enhanceH2Logs({ graphiqlUrl, rootDirectory });
41
+ enhanceH2Logs({ host, rootDirectory });
42
42
  console.warn(
43
43
  "[h2:warn:storefront.query] Wrong query.\nhttps://docs.com/something"
44
44
  );
@@ -50,7 +50,7 @@ describe("log replacer", () => {
50
50
  });
51
51
  describe("enhances h2:error pattern", () => {
52
52
  it("renders in an error banner", () => {
53
- enhanceH2Logs({ graphiqlUrl, rootDirectory });
53
+ enhanceH2Logs({ host, rootDirectory });
54
54
  console.error(new Error("[h2:error:storefront.query] Wrong query 2"));
55
55
  const error = outputMock.error();
56
56
  expect(error.split("stack trace:")[0]).not.toMatch("h2");
@@ -59,7 +59,7 @@ describe("log replacer", () => {
59
59
  expect(error).toMatch("Wrong query");
60
60
  });
61
61
  it("shows a GraphiQL link when the error is related to a GraphQL query", () => {
62
- enhanceH2Logs({ graphiqlUrl, rootDirectory });
62
+ enhanceH2Logs({ host, rootDirectory });
63
63
  console.error(
64
64
  new Error("[h2:error:storefront.query] Wrong query 3", {
65
65
  cause: {
@@ -75,7 +75,7 @@ describe("log replacer", () => {
75
75
  `);
76
76
  });
77
77
  it("trims stack traces when the error is related to a GraphQL query", () => {
78
- enhanceH2Logs({ graphiqlUrl, rootDirectory });
78
+ enhanceH2Logs({ host, rootDirectory });
79
79
  console.error(
80
80
  new Error("[h2:error:storefront.query] Wrong query 4", {
81
81
  cause: { graphql: { query: "query test {}" } }
@@ -1,6 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { AsyncLocalStorage } from 'node:async_hooks';
3
- import { resolvePath } from '@shopify/cli-kit/node/path';
4
3
  import { readFile } from '@shopify/cli-kit/node/fs';
5
4
  import { renderSuccess } from '@shopify/cli-kit/node/ui';
6
5
  import { startServer, Request } from '@shopify/mini-oxygen';
@@ -9,14 +8,12 @@ import { OXYGEN_HEADERS_MAP, logRequestLine } from './common.js';
9
8
  import { clearHistory, streamRequestEvents, logRequestEvent } from '../request-events.js';
10
9
 
11
10
  async function startNodeServer({
12
- root,
13
11
  port = DEFAULT_PORT,
14
12
  watch = false,
15
13
  buildPathWorkerFile,
16
14
  buildPathClient,
17
15
  env
18
16
  }) {
19
- resolvePath(root, ".env");
20
17
  const oxygenHeaders = Object.fromEntries(
21
18
  Object.entries(OXYGEN_HEADERS_MAP).map(([key, value]) => {
22
19
  return [key, value.defaultValue];
@@ -25,13 +22,13 @@ async function startNodeServer({
25
22
  const asyncLocalStorage = new AsyncLocalStorage();
26
23
  const serviceBindings = {
27
24
  H2O_LOG_EVENT: {
28
- fetch: (request) => logRequestEvent(
25
+ fetch: async (request) => logRequestEvent(
29
26
  new Request(request.url, {
30
- headers: {
31
- ...Object.fromEntries(request.headers.entries()),
32
- // Merge some headers from the parent request
27
+ method: "POST",
28
+ body: JSON.stringify({
29
+ ...await request.json(),
33
30
  ...asyncLocalStorage.getStore()
34
- }
31
+ })
35
32
  })
36
33
  )
37
34
  }
@@ -65,7 +62,7 @@ async function startNodeServer({
65
62
  }
66
63
  const startTimeMs = Date.now();
67
64
  const response = await asyncLocalStorage.run(
68
- { "request-id": requestId, purpose: request.headers.get("purpose") },
65
+ { requestId, purpose: request.headers.get("purpose") },
69
66
  () => defaultDispatcher(request)
70
67
  );
71
68
  logRequestLine(request, {
@@ -1,20 +1,24 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import { ReadableStream } from 'node:stream/web';
3
3
  import { Response } from '@shopify/mini-oxygen';
4
+ import { getGraphiQLUrl } from './graphiql-url.js';
4
5
 
5
6
  const DEV_ROUTES = /* @__PURE__ */ new Set(["/graphiql", "/debug-network"]);
6
7
  const EVENT_MAP = {
7
8
  request: "Request",
8
9
  subrequest: "Sub request"
9
10
  };
10
- function getRequestInfo(request) {
11
+ async function getRequestInfo(request) {
12
+ const data = await request.json();
11
13
  return {
12
- id: request.headers.get("request-id"),
13
- eventType: request.headers.get("hydrogen-event-type") || "unknown",
14
- startTime: request.headers.get("hydrogen-start-time"),
15
- endTime: request.headers.get("hydrogen-end-time") || String(Date.now()),
16
- purpose: request.headers.get("purpose") === "prefetch" ? "(prefetch)" : "",
17
- cacheStatus: request.headers.get("hydrogen-cache-status")
14
+ id: data.requestId ?? "",
15
+ eventType: data.eventType || "unknown",
16
+ startTime: data.startTime,
17
+ endTime: data.endTime || Date.now(),
18
+ purpose: data.purpose === "prefetch" ? "(prefetch)" : "",
19
+ cacheStatus: data.cacheStatus ?? "",
20
+ stackLine: data.stackLine ?? "",
21
+ graphql: data.graphql ? JSON.parse(data.graphql) : null
18
22
  };
19
23
  }
20
24
  const eventEmitter = new EventEmitter();
@@ -24,24 +28,36 @@ async function clearHistory() {
24
28
  return new Response("ok");
25
29
  }
26
30
  async function logRequestEvent(request) {
27
- if (DEV_ROUTES.has(new URL(request.url).pathname)) {
31
+ const url = new URL(request.url);
32
+ if (DEV_ROUTES.has(url.pathname)) {
28
33
  return new Response("ok");
29
34
  }
30
- const { eventType, purpose, ...data } = getRequestInfo(request);
35
+ const { eventType, purpose, stackLine, graphql, ...data } = await getRequestInfo(request);
36
+ let originFile = "";
37
+ let graphiqlLink = "";
31
38
  let description = request.url;
32
39
  if (eventType === "subrequest") {
33
- description = decodeURIComponent(request.url).match(/(query|mutation)\s+(\w+)/)?.[0]?.replace(/\s+/, " ") || request.url;
40
+ description = graphql?.query.match(/(query|mutation)\s+(\w+)/)?.[0]?.replace(/\s+/, " ") || decodeURIComponent(url.search.slice(1));
41
+ const [, fnName, filePath] = stackLine?.match(/\s+at ([^\s]+) \(.*?\/(app\/[^\n]*)\)/) || [];
42
+ if (fnName && filePath) {
43
+ originFile = `${fnName}:${filePath}`;
44
+ }
45
+ if (graphql) {
46
+ graphiqlLink = getGraphiQLUrl({ graphql });
47
+ }
34
48
  }
35
49
  const event = {
36
50
  event: EVENT_MAP[eventType] || eventType,
37
51
  data: JSON.stringify({
38
52
  ...data,
39
- url: `${purpose} ${description}`.trim()
53
+ url: `${purpose} ${description}`.trim(),
54
+ graphiqlLink,
55
+ originFile
40
56
  })
41
57
  };
58
+ eventHistory.push(event);
42
59
  if (eventHistory.length > 100)
43
60
  eventHistory.shift();
44
- eventHistory.push(event);
45
61
  eventEmitter.emit("request", event);
46
62
  return new Response("ok");
47
63
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "5.3.1",
2
+ "version": "5.4.0",
3
3
  "commands": {
4
4
  "hydrogen:build": {
5
5
  "id": "hydrogen:build",
@@ -28,6 +28,12 @@
28
28
  "description": "Show a bundle size summary after building.",
29
29
  "allowNo": true
30
30
  },
31
+ "lockfile-check": {
32
+ "name": "lockfile-check",
33
+ "type": "boolean",
34
+ "description": "Checks that there is exactly 1 valid lockfile in the project.",
35
+ "allowNo": true
36
+ },
31
37
  "disable-route-warning": {
32
38
  "name": "disable-route-warning",
33
39
  "type": "boolean",
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public",
5
5
  "@shopify:registry": "https://registry.npmjs.org"
6
6
  },
7
- "version": "5.3.1",
7
+ "version": "5.4.0",
8
8
  "license": "MIT",
9
9
  "type": "module",
10
10
  "scripts": {