@shopify/cli-hydrogen 5.3.1 → 5.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  });
@@ -4,9 +4,9 @@ import { linkStorefront } from '../commands/hydrogen/link.js';
4
4
  import { login } from './auth.js';
5
5
  import { getCliCommand } from './shell.js';
6
6
  import { renderMissingLink, renderMissingStorefront } from './render-errors.js';
7
- import { getOxygenToken } from './graphql/admin/oxygen-token.js';
7
+ import { getOxygenData } from './graphql/admin/get-oxygen-data.js';
8
8
 
9
- async function getOxygenDeploymentToken({
9
+ async function getOxygenDeploymentData({
10
10
  root
11
11
  }) {
12
12
  const [{ session, config }, cliCommand] = await Promise.all([
@@ -28,7 +28,7 @@ async function getOxygenDeploymentToken({
28
28
  if (!config.storefront) {
29
29
  return;
30
30
  }
31
- const { storefront } = await getOxygenToken(session, config.storefront.id);
31
+ const { storefront } = await getOxygenData(session, config.storefront.id);
32
32
  if (!storefront) {
33
33
  renderMissingStorefront({
34
34
  session,
@@ -41,7 +41,7 @@ async function getOxygenDeploymentToken({
41
41
  outputWarn(`Could not retrieve a deployment token.`);
42
42
  return;
43
43
  }
44
- return storefront.oxygenDeploymentToken;
44
+ return storefront;
45
45
  }
46
46
 
47
- export { getOxygenDeploymentToken };
47
+ export { getOxygenDeploymentData };
@@ -1,11 +1,11 @@
1
1
  import { vi, describe, beforeEach, afterEach, it, expect } from 'vitest';
2
2
  import { renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
3
- import { getOxygenDeploymentToken } from './get-oxygen-token.js';
3
+ import { getOxygenDeploymentData } from './get-oxygen-deployment-data.js';
4
4
  import { login } from './auth.js';
5
5
  import { getConfig } from './shopify-config.js';
6
6
  import { renderMissingLink, renderMissingStorefront } from './render-errors.js';
7
7
  import { linkStorefront } from '../commands/hydrogen/link.js';
8
- import { getOxygenToken } from './graphql/admin/oxygen-token.js';
8
+ import { getOxygenData } from './graphql/admin/get-oxygen-data.js';
9
9
 
10
10
  vi.mock("@shopify/cli-kit/node/ui", async () => {
11
11
  const original = await vi.importActual("@shopify/cli-kit/node/ui");
@@ -19,9 +19,19 @@ vi.mock("./admin-session.js");
19
19
  vi.mock("./shopify-config.js");
20
20
  vi.mock("./render-errors.js");
21
21
  vi.mock("../commands/hydrogen/link.js");
22
- vi.mock("./graphql/admin/oxygen-token.js");
23
- describe("getOxygenDeploymentToken", () => {
22
+ vi.mock("./graphql/admin/get-oxygen-data.js");
23
+ describe("getOxygenDeploymentData", () => {
24
24
  const OXYGEN_DEPLOYMENT_TOKEN = "a-lovely-token";
25
+ const environments = [
26
+ {
27
+ name: "production",
28
+ branch: "main"
29
+ },
30
+ {
31
+ name: "preview",
32
+ branch: "staging"
33
+ }
34
+ ];
25
35
  beforeEach(() => {
26
36
  vi.mocked(login).mockResolvedValue({
27
37
  session: {
@@ -41,16 +51,20 @@ describe("getOxygenDeploymentToken", () => {
41
51
  vi.mocked(getConfig).mockResolvedValue({
42
52
  storefront: { id: "storefront-id", title: "Existing Link" }
43
53
  });
44
- vi.mocked(getOxygenToken).mockResolvedValue({
45
- storefront: { oxygenDeploymentToken: OXYGEN_DEPLOYMENT_TOKEN }
54
+ vi.mocked(getOxygenData).mockResolvedValue({
55
+ storefront: {
56
+ oxygenDeploymentToken: OXYGEN_DEPLOYMENT_TOKEN,
57
+ environments
58
+ }
46
59
  });
47
60
  });
48
61
  afterEach(() => {
49
62
  vi.resetAllMocks();
50
63
  });
51
- it("returns the oxygen deployment token", async () => {
52
- const token = await getOxygenDeploymentToken({ root: "test-root" });
53
- expect(token).toBe(OXYGEN_DEPLOYMENT_TOKEN);
64
+ it("returns the oxygen deployment token and environments", async () => {
65
+ const data = await getOxygenDeploymentData({ root: "test-root" });
66
+ expect(data?.oxygenDeploymentToken).toBe(OXYGEN_DEPLOYMENT_TOKEN);
67
+ expect(data?.environments).toEqual(environments);
54
68
  });
55
69
  describe("when there is no linked storefront", () => {
56
70
  beforeEach(() => {
@@ -69,36 +83,36 @@ describe("getOxygenDeploymentToken", () => {
69
83
  });
70
84
  it("calls renderMissingLink and prompts the user to create a link", async () => {
71
85
  vi.mocked(renderConfirmationPrompt).mockResolvedValue(true);
72
- await getOxygenDeploymentToken({ root: "test-root" });
86
+ await getOxygenDeploymentData({ root: "test-root" });
73
87
  expect(renderMissingLink).toHaveBeenCalled();
74
88
  expect(renderConfirmationPrompt).toHaveBeenCalled();
75
89
  expect(linkStorefront).toHaveBeenCalled();
76
90
  });
77
91
  it("returns nothing if the user does not create a new link", async () => {
78
92
  vi.mocked(renderConfirmationPrompt).mockResolvedValue(false);
79
- const token = await getOxygenDeploymentToken({ root: "test-root" });
93
+ const token = await getOxygenDeploymentData({ root: "test-root" });
80
94
  expect(token).toEqual(void 0);
81
95
  });
82
96
  });
83
97
  describe("when there is no matching storefront in the shop", () => {
84
98
  beforeEach(() => {
85
- vi.mocked(getOxygenToken).mockResolvedValue({ storefront: null });
99
+ vi.mocked(getOxygenData).mockResolvedValue({ storefront: null });
86
100
  });
87
101
  it("calls renderMissingStorefront and returns nothing", async () => {
88
- const token = await getOxygenDeploymentToken({ root: "test-root" });
102
+ const token = await getOxygenDeploymentData({ root: "test-root" });
89
103
  expect(renderMissingStorefront).toHaveBeenCalled();
90
104
  expect(token).toEqual(void 0);
91
105
  });
92
106
  });
93
107
  describe("when the storefront does not have an oxygen deployment token", () => {
94
108
  beforeEach(() => {
95
- vi.mocked(getOxygenToken).mockResolvedValue({
96
- storefront: { oxygenDeploymentToken: "" }
109
+ vi.mocked(getOxygenData).mockResolvedValue({
110
+ storefront: { oxygenDeploymentToken: "", environments: [] }
97
111
  });
98
112
  });
99
113
  it("returns nothing", async () => {
100
- const token = await getOxygenDeploymentToken({ root: "test-root" });
101
- expect(token).toEqual(void 0);
114
+ const data = await getOxygenDeploymentData({ root: "test-root" });
115
+ expect(data).toEqual(void 0);
102
116
  });
103
117
  });
104
118
  });
@@ -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 };
@@ -1,15 +1,19 @@
1
1
  import { adminRequest } from './client.js';
2
2
 
3
- const GetDeploymentTokenQuery = `#graphql
3
+ const GetDeploymentDataQuery = `#graphql
4
4
  query GetDeploymentToken($id: ID!) {
5
5
  hydrogenStorefront(id: $id) {
6
6
  oxygenDeploymentToken
7
+ environments {
8
+ name
9
+ branch
10
+ }
7
11
  }
8
12
  }
9
13
  `;
10
- async function getOxygenToken(adminSession, storefrontId) {
14
+ async function getOxygenData(adminSession, storefrontId) {
11
15
  const { hydrogenStorefront } = await adminRequest(
12
- GetDeploymentTokenQuery,
16
+ GetDeploymentDataQuery,
13
17
  adminSession,
14
18
  {
15
19
  id: storefrontId
@@ -18,4 +22,4 @@ async function getOxygenToken(adminSession, storefrontId) {
18
22
  return { storefront: hydrogenStorefront };
19
23
  }
20
24
 
21
- export { GetDeploymentTokenQuery, getOxygenToken };
25
+ export { GetDeploymentDataQuery, getOxygenData };
@@ -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.1",
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",
@@ -147,6 +153,14 @@
147
153
  "hidden": true,
148
154
  "aliases": [],
149
155
  "flags": {
156
+ "env-branch": {
157
+ "name": "env-branch",
158
+ "type": "option",
159
+ "char": "e",
160
+ "description": "Environment branch (tag) for environment to deploy to",
161
+ "required": false,
162
+ "multiple": false
163
+ },
150
164
  "path": {
151
165
  "name": "path",
152
166
  "type": "option",
@@ -160,29 +174,37 @@
160
174
  "description": "Shop URL. It can be the shop prefix (janes-apparel) or the full myshopify.com URL (janes-apparel.myshopify.com, https://janes-apparel.myshopify.com).",
161
175
  "multiple": false
162
176
  },
163
- "publicDeployment": {
164
- "name": "publicDeployment",
177
+ "public-deployment": {
178
+ "name": "public-deployment",
165
179
  "type": "boolean",
166
180
  "description": "Marks a preview deployment as publicly accessible.",
167
181
  "required": false,
168
182
  "allowNo": false
169
183
  },
170
- "metadataUrl": {
171
- "name": "metadataUrl",
184
+ "token": {
185
+ "name": "token",
186
+ "type": "option",
187
+ "char": "t",
188
+ "description": "Oxygen deployment token",
189
+ "required": false,
190
+ "multiple": false
191
+ },
192
+ "metadata-url": {
193
+ "name": "metadata-url",
172
194
  "type": "option",
173
195
  "description": "URL that links to the deployment. Will be saved and displayed in the Shopify admin",
174
196
  "required": false,
175
197
  "multiple": false
176
198
  },
177
- "metadataUser": {
178
- "name": "metadataUser",
199
+ "metadata-user": {
200
+ "name": "metadata-user",
179
201
  "type": "option",
180
202
  "description": "User that initiated the deployment. Will be saved and displayed in the Shopify admin",
181
203
  "required": false,
182
204
  "multiple": false
183
205
  },
184
- "metadataVersion": {
185
- "name": "metadataVersion",
206
+ "metadata-version": {
207
+ "name": "metadata-version",
186
208
  "type": "option",
187
209
  "description": "A version identifier for the deployment. Will be saved and displayed in the Shopify admin",
188
210
  "required": false,