@shopify/cli-hydrogen 4.1.1 → 4.1.2

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 (51) hide show
  1. package/dist/commands/hydrogen/dev.d.ts +1 -0
  2. package/dist/commands/hydrogen/dev.js +8 -0
  3. package/dist/commands/hydrogen/env/pull.d.ts +21 -0
  4. package/dist/commands/hydrogen/env/pull.js +115 -0
  5. package/dist/commands/hydrogen/env/pull.test.d.ts +1 -0
  6. package/dist/commands/hydrogen/env/pull.test.js +205 -0
  7. package/dist/commands/hydrogen/init.js +3 -1
  8. package/dist/commands/hydrogen/link.d.ts +24 -0
  9. package/dist/commands/hydrogen/link.js +102 -0
  10. package/dist/commands/hydrogen/link.test.d.ts +1 -0
  11. package/dist/commands/hydrogen/link.test.js +137 -0
  12. package/dist/commands/hydrogen/list.d.ts +21 -0
  13. package/dist/commands/hydrogen/list.js +83 -0
  14. package/dist/commands/hydrogen/list.test.d.ts +1 -0
  15. package/dist/commands/hydrogen/list.test.js +116 -0
  16. package/dist/commands/hydrogen/unlink.d.ts +17 -0
  17. package/dist/commands/hydrogen/unlink.js +29 -0
  18. package/dist/commands/hydrogen/unlink.test.d.ts +1 -0
  19. package/dist/commands/hydrogen/unlink.test.js +36 -0
  20. package/dist/generator-templates/routes/[robots.txt].tsx +111 -19
  21. package/dist/generator-templates/routes/collections/index.tsx +102 -0
  22. package/dist/lib/admin-session.d.ts +5 -0
  23. package/dist/lib/admin-session.js +16 -0
  24. package/dist/lib/admin-session.test.d.ts +1 -0
  25. package/dist/lib/admin-session.test.js +27 -0
  26. package/dist/lib/admin-urls.d.ts +8 -0
  27. package/dist/lib/admin-urls.js +18 -0
  28. package/dist/lib/flags.d.ts +1 -0
  29. package/dist/lib/flags.js +7 -0
  30. package/dist/lib/graphql/admin/link-storefront.d.ts +11 -0
  31. package/dist/lib/graphql/admin/link-storefront.js +11 -0
  32. package/dist/lib/graphql/admin/list-storefronts.d.ts +17 -0
  33. package/dist/lib/graphql/admin/list-storefronts.js +16 -0
  34. package/dist/lib/graphql/admin/pull-variables.d.ts +16 -0
  35. package/dist/lib/graphql/admin/pull-variables.js +15 -0
  36. package/dist/lib/graphql.d.ts +21 -0
  37. package/dist/lib/graphql.js +18 -0
  38. package/dist/lib/graphql.test.d.ts +1 -0
  39. package/dist/lib/graphql.test.js +15 -0
  40. package/dist/lib/missing-storefronts.d.ts +5 -0
  41. package/dist/lib/missing-storefronts.js +18 -0
  42. package/dist/lib/shop.d.ts +7 -0
  43. package/dist/lib/shop.js +32 -0
  44. package/dist/lib/shop.test.d.ts +1 -0
  45. package/dist/lib/shop.test.js +78 -0
  46. package/dist/lib/shopify-config.d.ts +35 -0
  47. package/dist/lib/shopify-config.js +86 -0
  48. package/dist/lib/shopify-config.test.d.ts +1 -0
  49. package/dist/lib/shopify-config.test.js +209 -0
  50. package/oclif.manifest.json +1 -1
  51. package/package.json +4 -4
@@ -7,6 +7,7 @@ declare class Dev extends Command {
7
7
  path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
8
8
  port: _oclif_core_lib_interfaces_parser_js.OptionFlag<number, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
9
9
  "disable-virtual-routes": _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
10
+ debug: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
10
11
  host: _oclif_core_lib_interfaces_parser_js.OptionFlag<unknown, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
11
12
  };
12
13
  run(): Promise<void>;
@@ -25,6 +25,11 @@ class Dev extends Command {
25
25
  env: "SHOPIFY_HYDROGEN_FLAG_DISABLE_VIRTUAL_ROUTES",
26
26
  default: false
27
27
  }),
28
+ debug: Flags.boolean({
29
+ description: "Attaches a Node inspector",
30
+ env: "SHOPIFY_HYDROGEN_FLAG_DEBUG",
31
+ default: false
32
+ }),
28
33
  host: deprecated("--host")()
29
34
  };
30
35
  async run() {
@@ -36,11 +41,14 @@ class Dev extends Command {
36
41
  async function runDev({
37
42
  port,
38
43
  path: appPath,
44
+ debug = false,
39
45
  disableVirtualRoutes
40
46
  }) {
41
47
  if (!process.env.NODE_ENV)
42
48
  process.env.NODE_ENV = "development";
43
49
  muteDevLogs();
50
+ if (debug)
51
+ (await import('node:inspector')).open();
44
52
  console.time(LOG_INITIAL_BUILD);
45
53
  const { root, publicPath, buildPathClient, buildPathWorkerFile } = getProjectPaths(appPath);
46
54
  const checkingHydrogenVersion = checkHydrogenVersion(root);
@@ -0,0 +1,21 @@
1
+ import * as _oclif_core_lib_interfaces_parser_js from '@oclif/core/lib/interfaces/parser.js';
2
+ import Command from '@shopify/cli-kit/node/base-command';
3
+
4
+ declare class Pull extends Command {
5
+ static description: string;
6
+ static hidden: boolean;
7
+ static flags: {
8
+ path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
9
+ shop: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
10
+ force: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
11
+ };
12
+ run(): Promise<void>;
13
+ }
14
+ interface Flags {
15
+ force?: boolean;
16
+ path?: string;
17
+ shop?: string;
18
+ }
19
+ declare function pullVariables({ force, path, shop: flagShop }: Flags): Promise<void>;
20
+
21
+ export { Pull as default, pullVariables };
@@ -0,0 +1,115 @@
1
+ import Command from '@shopify/cli-kit/node/base-command';
2
+ import { renderFatalError, renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
3
+ import { outputContent, outputToken, outputInfo, outputWarn, outputSuccess } from '@shopify/cli-kit/node/output';
4
+ import { fileExists, writeFile } from '@shopify/cli-kit/node/fs';
5
+ import { resolvePath } from '@shopify/cli-kit/node/path';
6
+ import { linkStorefront } from '../link.js';
7
+ import { adminRequest, parseGid } from '../../../lib/graphql.js';
8
+ import { commonFlags } from '../../../lib/flags.js';
9
+ import { getHydrogenShop } from '../../../lib/shop.js';
10
+ import { getAdminSession } from '../../../lib/admin-session.js';
11
+ import { PullVariablesQuery } from '../../../lib/graphql/admin/pull-variables.js';
12
+ import { getConfig } from '../../../lib/shopify-config.js';
13
+ import { hydrogenStorefrontsUrl } from '../../../lib/admin-urls.js';
14
+
15
+ class Pull extends Command {
16
+ static description = "Populate your .env with variables from your Hydrogen storefront.";
17
+ static hidden = true;
18
+ static flags = {
19
+ path: commonFlags.path,
20
+ shop: commonFlags.shop,
21
+ force: commonFlags.force
22
+ };
23
+ async run() {
24
+ const { flags } = await this.parse(Pull);
25
+ await pullVariables(flags);
26
+ }
27
+ }
28
+ async function pullVariables({ force, path, shop: flagShop }) {
29
+ const shop = await getHydrogenShop({ path, shop: flagShop });
30
+ const adminSession = await getAdminSession(shop);
31
+ const actualPath = path ?? process.cwd();
32
+ let configStorefront = (await getConfig(actualPath)).storefront;
33
+ if (!configStorefront?.id) {
34
+ renderFatalError({
35
+ name: "NoLinkedStorefrontError",
36
+ type: 0,
37
+ message: `No linked Hydrogen storefront on ${adminSession.storeFqdn}`,
38
+ tryMessage: outputContent`To pull environment variables, link this project to a Hydrogen storefront. To select a storefront to link, run ${outputToken.genericShellCommand(
39
+ `npx shopify hydrogen link`
40
+ )}.`.value
41
+ });
42
+ const runLink = await renderConfirmationPrompt({
43
+ message: outputContent`Run ${outputToken.genericShellCommand(
44
+ `npx shopify hydrogen link`
45
+ )}?`.value
46
+ });
47
+ if (!runLink) {
48
+ return;
49
+ }
50
+ await linkStorefront({ force, path, shop: flagShop, silent: true });
51
+ }
52
+ configStorefront = (await getConfig(actualPath)).storefront;
53
+ if (!configStorefront) {
54
+ return;
55
+ }
56
+ outputInfo(
57
+ `Fetching Preview environment variables from ${configStorefront.title}...`
58
+ );
59
+ const result = await adminRequest(
60
+ PullVariablesQuery,
61
+ adminSession,
62
+ {
63
+ id: configStorefront.id
64
+ }
65
+ );
66
+ const hydrogenStorefront = result.hydrogenStorefront;
67
+ if (!hydrogenStorefront) {
68
+ renderFatalError({
69
+ name: "NoStorefrontError",
70
+ type: 0,
71
+ message: outputContent`${outputToken.errorText(
72
+ "Couldn\u2019t find Hydrogen storefront."
73
+ )}`.value,
74
+ tryMessage: outputContent`Couldn’t find ${configStorefront.title} (ID: ${parseGid(configStorefront.id)}) on ${adminSession.storeFqdn}. Check that the storefront exists and run ${outputToken.genericShellCommand(
75
+ `npx shopify hydrogen link`
76
+ )} to link this project to it.\n\n${outputToken.link(
77
+ "Hydrogen Storefronts Admin",
78
+ hydrogenStorefrontsUrl(adminSession)
79
+ )}`.value
80
+ });
81
+ return;
82
+ }
83
+ if (!hydrogenStorefront.environmentVariables.length) {
84
+ outputInfo(`No Preview environment variables found.`);
85
+ return;
86
+ }
87
+ const dotEnvPath = resolvePath(actualPath, ".env");
88
+ if (await fileExists(dotEnvPath) && !force) {
89
+ const overwrite = await renderConfirmationPrompt({
90
+ message: "Warning: .env file already exists. Do you want to overwrite it?"
91
+ });
92
+ if (!overwrite) {
93
+ return;
94
+ }
95
+ }
96
+ let hasSecretVariables = false;
97
+ const contents = hydrogenStorefront.environmentVariables.map(({ key, value, isSecret }) => {
98
+ let line = `${key}="${value}"`;
99
+ if (isSecret) {
100
+ hasSecretVariables = true;
101
+ line = `# ${key} is marked as secret and its value is hidden
102
+ ` + line;
103
+ }
104
+ return line;
105
+ }).join("\n") + "\n";
106
+ if (hasSecretVariables) {
107
+ outputWarn(
108
+ `${configStorefront.title} contains environment variables marked as secret, so their values weren\u2019t pulled.`
109
+ );
110
+ }
111
+ await writeFile(dotEnvPath, contents);
112
+ outputSuccess("Updated .env");
113
+ }
114
+
115
+ export { Pull as default, pullVariables };
@@ -0,0 +1,205 @@
1
+ import { vi, describe, beforeEach, afterEach, it, expect } from 'vitest';
2
+ import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output';
3
+ import { inTemporaryDirectory, fileExists, readFile, writeFile } from '@shopify/cli-kit/node/fs';
4
+ import { joinPath } from '@shopify/cli-kit/node/path';
5
+ import { renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
6
+ import { PullVariablesQuery } from '../../../lib/graphql/admin/pull-variables.js';
7
+ import { getAdminSession } from '../../../lib/admin-session.js';
8
+ import { adminRequest } from '../../../lib/graphql.js';
9
+ import { getConfig } from '../../../lib/shopify-config.js';
10
+ import { linkStorefront } from '../link.js';
11
+ import { pullVariables } from './pull.js';
12
+
13
+ vi.mock("@shopify/cli-kit/node/ui", async () => {
14
+ const original = await vi.importActual("@shopify/cli-kit/node/ui");
15
+ return {
16
+ ...original,
17
+ renderConfirmationPrompt: vi.fn()
18
+ };
19
+ });
20
+ vi.mock("../link.js");
21
+ vi.mock("../../../lib/admin-session.js");
22
+ vi.mock("../../../lib/shopify-config.js");
23
+ vi.mock("../../../lib/graphql.js", async () => {
24
+ const original = await vi.importActual("../../../lib/graphql.js");
25
+ return {
26
+ ...original,
27
+ adminRequest: vi.fn()
28
+ };
29
+ });
30
+ vi.mock("../../../lib/shop.js", () => ({
31
+ getHydrogenShop: () => "my-shop"
32
+ }));
33
+ describe("pullVariables", () => {
34
+ const ADMIN_SESSION = {
35
+ token: "abc123",
36
+ storeFqdn: "my-shop"
37
+ };
38
+ beforeEach(async () => {
39
+ vi.mocked(getAdminSession).mockResolvedValue(ADMIN_SESSION);
40
+ vi.mocked(getConfig).mockResolvedValue({
41
+ storefront: {
42
+ id: "gid://shopify/HydrogenStorefront/2",
43
+ title: "Existing Link"
44
+ }
45
+ });
46
+ vi.mocked(adminRequest).mockResolvedValue({
47
+ hydrogenStorefront: {
48
+ id: "gid://shopify/HydrogenStorefront/1",
49
+ environmentVariables: [
50
+ {
51
+ id: "gid://shopify/HydrogenStorefrontEnvironmentVariable/1",
52
+ key: "PUBLIC_API_TOKEN",
53
+ value: "abc123",
54
+ isSecret: false
55
+ },
56
+ {
57
+ id: "gid://shopify/HydrogenStorefrontEnvironmentVariable/1",
58
+ key: "PRIVATE_API_TOKEN",
59
+ value: "",
60
+ isSecret: true
61
+ }
62
+ ]
63
+ }
64
+ });
65
+ });
66
+ afterEach(() => {
67
+ vi.resetAllMocks();
68
+ mockAndCaptureOutput().clear();
69
+ });
70
+ it("makes a GraphQL call to fetch environment variables", async () => {
71
+ await inTemporaryDirectory(async (tmpDir) => {
72
+ await pullVariables({ path: tmpDir });
73
+ expect(adminRequest).toHaveBeenCalledWith(
74
+ PullVariablesQuery,
75
+ ADMIN_SESSION,
76
+ {
77
+ id: "gid://shopify/HydrogenStorefront/2"
78
+ }
79
+ );
80
+ });
81
+ });
82
+ it("writes environment variables to a .env file", async () => {
83
+ await inTemporaryDirectory(async (tmpDir) => {
84
+ const filePath = joinPath(tmpDir, ".env");
85
+ expect(await fileExists(filePath)).toBeFalsy();
86
+ await pullVariables({ path: tmpDir });
87
+ expect(await readFile(filePath)).toStrictEqual(
88
+ 'PUBLIC_API_TOKEN="abc123"\n# PRIVATE_API_TOKEN is marked as secret and its value is hidden\nPRIVATE_API_TOKEN=""\n'
89
+ );
90
+ });
91
+ });
92
+ it("warns if there are any variables marked as secret", async () => {
93
+ vi.mocked(adminRequest).mockResolvedValue({
94
+ hydrogenStorefront: {
95
+ id: "gid://shopify/HydrogenStorefront/1",
96
+ environmentVariables: [
97
+ {
98
+ id: "gid://shopify/HydrogenStorefrontEnvironmentVariable/1",
99
+ key: "PRIVATE_API_TOKEN",
100
+ value: "",
101
+ isSecret: true
102
+ }
103
+ ]
104
+ }
105
+ });
106
+ await inTemporaryDirectory(async (tmpDir) => {
107
+ const outputMock = mockAndCaptureOutput();
108
+ await pullVariables({ path: tmpDir });
109
+ expect(outputMock.warn()).toStrictEqual(
110
+ "Existing Link contains environment variables marked as secret, so their values weren\u2019t pulled."
111
+ );
112
+ });
113
+ });
114
+ describe("when a .env file already exists", () => {
115
+ beforeEach(() => {
116
+ vi.mocked(renderConfirmationPrompt).mockResolvedValue(true);
117
+ });
118
+ it("prompts the user to confirm", async () => {
119
+ await inTemporaryDirectory(async (tmpDir) => {
120
+ const filePath = joinPath(tmpDir, ".env");
121
+ await writeFile(filePath, "EXISTING_TOKEN=1");
122
+ await pullVariables({ path: tmpDir });
123
+ expect(renderConfirmationPrompt).toHaveBeenCalledWith({
124
+ message: expect.stringMatching(
125
+ /Warning: \.env file already exists\. Do you want to overwrite it\?/
126
+ )
127
+ });
128
+ });
129
+ });
130
+ describe("and --force is enabled", () => {
131
+ it("does not prompt the user to confirm", async () => {
132
+ await inTemporaryDirectory(async (tmpDir) => {
133
+ const filePath = joinPath(tmpDir, ".env");
134
+ await writeFile(filePath, "EXISTING_TOKEN=1");
135
+ await pullVariables({ path: tmpDir, force: true });
136
+ expect(renderConfirmationPrompt).not.toHaveBeenCalled();
137
+ });
138
+ });
139
+ });
140
+ });
141
+ describe("when there are no environment variables to update", () => {
142
+ beforeEach(() => {
143
+ vi.mocked(adminRequest).mockResolvedValue({
144
+ hydrogenStorefront: {
145
+ id: "gid://shopify/HydrogenStorefront/1",
146
+ environmentVariables: []
147
+ }
148
+ });
149
+ });
150
+ it("renders a message", async () => {
151
+ await inTemporaryDirectory(async (tmpDir) => {
152
+ const outputMock = mockAndCaptureOutput();
153
+ await pullVariables({ path: tmpDir });
154
+ expect(outputMock.info()).toMatch(
155
+ /No Preview environment variables found\./
156
+ );
157
+ });
158
+ });
159
+ });
160
+ describe("when there is no linked storefront", () => {
161
+ beforeEach(() => {
162
+ vi.mocked(getConfig).mockResolvedValue({
163
+ storefront: void 0
164
+ });
165
+ });
166
+ it("renders an error message", async () => {
167
+ await inTemporaryDirectory(async (tmpDir) => {
168
+ const outputMock = mockAndCaptureOutput();
169
+ await pullVariables({ path: tmpDir });
170
+ expect(outputMock.error()).toMatch(
171
+ /No linked Hydrogen storefront on my-shop/
172
+ );
173
+ });
174
+ });
175
+ it("prompts the user to create a link", async () => {
176
+ vi.mocked(renderConfirmationPrompt).mockResolvedValue(true);
177
+ await inTemporaryDirectory(async (tmpDir) => {
178
+ await pullVariables({ path: tmpDir });
179
+ expect(renderConfirmationPrompt).toHaveBeenCalledWith({
180
+ message: expect.stringMatching(/Run .*npx shopify hydrogen link.*\?/)
181
+ });
182
+ expect(linkStorefront).toHaveBeenCalledWith({
183
+ path: tmpDir,
184
+ silent: true
185
+ });
186
+ });
187
+ });
188
+ });
189
+ describe("when there is no matching storefront in the shop", () => {
190
+ beforeEach(() => {
191
+ vi.mocked(adminRequest).mockResolvedValue({
192
+ hydrogenStorefront: null
193
+ });
194
+ });
195
+ it("renders an error message", async () => {
196
+ await inTemporaryDirectory(async (tmpDir) => {
197
+ const outputMock = mockAndCaptureOutput();
198
+ await pullVariables({ path: tmpDir });
199
+ expect(outputMock.error()).toMatch(
200
+ /Couldn’t find Hydrogen storefront\./
201
+ );
202
+ });
203
+ });
204
+ });
205
+ });
@@ -152,7 +152,9 @@ async function runInit(options = parseProcessFlags(process.argv, FLAG_MAP)) {
152
152
  )} to start your local development server and start building`.value
153
153
  ].filter((step) => Boolean(step)),
154
154
  reference: [
155
- "Building with Hydrogen: https://shopify.dev/docs/custom-storefronts/hydrogen/building/begin-development"
155
+ "Getting started with Hydrogen: https://shopify.dev/docs/custom-storefronts/hydrogen/building/begin-development",
156
+ "Hydrogen project structure: https://shopify.dev/docs/custom-storefronts/hydrogen/project-structure",
157
+ "Setting up Hydrogen environment variables: https://shopify.dev/docs/custom-storefronts/hydrogen/environment-variables"
156
158
  ]
157
159
  });
158
160
  if (appTemplate === "demo-store") {
@@ -0,0 +1,24 @@
1
+ import * as _oclif_core_lib_interfaces_parser_js from '@oclif/core/lib/interfaces/parser.js';
2
+ import Command from '@shopify/cli-kit/node/base-command';
3
+
4
+ declare class Link extends Command {
5
+ static description: string;
6
+ static hidden: boolean;
7
+ static flags: {
8
+ force: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
9
+ path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
10
+ shop: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
11
+ storefront: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
12
+ };
13
+ run(): Promise<void>;
14
+ }
15
+ interface LinkStorefrontArguments {
16
+ force?: boolean;
17
+ path?: string;
18
+ shop?: string;
19
+ storefront?: string;
20
+ silent?: boolean;
21
+ }
22
+ declare function linkStorefront({ force, path, shop: flagShop, storefront: flagStorefront, silent, }: LinkStorefrontArguments): Promise<void>;
23
+
24
+ export { LinkStorefrontArguments, Link as default, linkStorefront };
@@ -0,0 +1,102 @@
1
+ import { Flags } from '@oclif/core';
2
+ import Command from '@shopify/cli-kit/node/base-command';
3
+ import { renderConfirmationPrompt, renderWarning, renderSelectPrompt } from '@shopify/cli-kit/node/ui';
4
+ import { outputContent, outputToken, outputSuccess, outputInfo } from '@shopify/cli-kit/node/output';
5
+ import { adminRequest, parseGid } from '../../lib/graphql.js';
6
+ import { commonFlags } from '../../lib/flags.js';
7
+ import { getHydrogenShop } from '../../lib/shop.js';
8
+ import { getAdminSession } from '../../lib/admin-session.js';
9
+ import { hydrogenStorefrontUrl } from '../../lib/admin-urls.js';
10
+ import { LinkStorefrontQuery } from '../../lib/graphql/admin/link-storefront.js';
11
+ import { getConfig, setStorefront } from '../../lib/shopify-config.js';
12
+ import { logMissingStorefronts } from '../../lib/missing-storefronts.js';
13
+
14
+ class Link extends Command {
15
+ static description = "Link a local project to one of your shop's Hydrogen storefronts.";
16
+ static hidden = true;
17
+ static flags = {
18
+ force: commonFlags.force,
19
+ path: commonFlags.path,
20
+ shop: commonFlags.shop,
21
+ storefront: Flags.string({
22
+ char: "h",
23
+ description: `The name of a Hydrogen Storefront (e.g. "Jane's Apparel")`,
24
+ env: "SHOPIFY_HYDROGEN_STOREFRONT"
25
+ })
26
+ };
27
+ async run() {
28
+ const { flags } = await this.parse(Link);
29
+ await linkStorefront(flags);
30
+ }
31
+ }
32
+ async function linkStorefront({
33
+ force,
34
+ path,
35
+ shop: flagShop,
36
+ storefront: flagStorefront,
37
+ silent = false
38
+ }) {
39
+ const shop = await getHydrogenShop({ path, shop: flagShop });
40
+ const { storefront: configStorefront } = await getConfig(path ?? process.cwd());
41
+ if (configStorefront && !force) {
42
+ const overwriteLink = await renderConfirmationPrompt({
43
+ message: `Your project is currently linked to ${configStorefront.title}. Do you want to link to a different Hydrogen storefront on Shopify?`
44
+ });
45
+ if (!overwriteLink) {
46
+ return;
47
+ }
48
+ }
49
+ const adminSession = await getAdminSession(shop);
50
+ const result = await adminRequest(
51
+ LinkStorefrontQuery,
52
+ adminSession
53
+ );
54
+ if (!result.hydrogenStorefronts.length) {
55
+ logMissingStorefronts(adminSession);
56
+ return;
57
+ }
58
+ let selectedStorefront;
59
+ if (flagStorefront) {
60
+ selectedStorefront = result.hydrogenStorefronts.find(
61
+ (storefront) => storefront.title === flagStorefront
62
+ );
63
+ if (!selectedStorefront) {
64
+ renderWarning({
65
+ headline: `Couldn't find ${flagStorefront}`,
66
+ body: outputContent`There's no storefront matching ${flagStorefront} on your ${shop} shop. To see all available Hydrogen storefronts, run ${outputToken.genericShellCommand(
67
+ `npx shopify hydrogen list`
68
+ )}`.value
69
+ });
70
+ return;
71
+ }
72
+ } else {
73
+ const choices = result.hydrogenStorefronts.map((storefront) => ({
74
+ label: `${storefront.title} ${storefront.productionUrl}${storefront.id === configStorefront?.id ? " (Current)" : ""}`,
75
+ value: storefront.id
76
+ }));
77
+ const storefrontId = await renderSelectPrompt({
78
+ message: "Choose a Hydrogen storefront to link this project to:",
79
+ choices,
80
+ defaultValue: "true"
81
+ });
82
+ selectedStorefront = result.hydrogenStorefronts.find(
83
+ (storefront) => storefront.id === storefrontId
84
+ );
85
+ }
86
+ if (!selectedStorefront) {
87
+ return;
88
+ }
89
+ await setStorefront(path ?? process.cwd(), selectedStorefront);
90
+ outputSuccess(`Linked to ${selectedStorefront.title}`);
91
+ if (!silent) {
92
+ outputInfo(
93
+ `Admin URL: ${hydrogenStorefrontUrl(
94
+ adminSession,
95
+ parseGid(selectedStorefront.id)
96
+ )}`
97
+ );
98
+ outputInfo(`Site URL: ${selectedStorefront.productionUrl}`);
99
+ }
100
+ }
101
+
102
+ export { Link as default, linkStorefront };
@@ -0,0 +1,137 @@
1
+ import { vi, describe, beforeEach, afterEach, it, expect } from 'vitest';
2
+ import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output';
3
+ import { renderSelectPrompt, renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
4
+ import { adminRequest } from '../../lib/graphql.js';
5
+ import { LinkStorefrontQuery } from '../../lib/graphql/admin/link-storefront.js';
6
+ import { getAdminSession } from '../../lib/admin-session.js';
7
+ import { getConfig, setStorefront } from '../../lib/shopify-config.js';
8
+ import { linkStorefront } from './link.js';
9
+
10
+ vi.mock("@shopify/cli-kit/node/ui", async () => {
11
+ const original = await vi.importActual("@shopify/cli-kit/node/ui");
12
+ return {
13
+ ...original,
14
+ renderConfirmationPrompt: vi.fn(),
15
+ renderSelectPrompt: vi.fn()
16
+ };
17
+ });
18
+ vi.mock("../../lib/graphql.js");
19
+ vi.mock("../../lib/shopify-config.js");
20
+ vi.mock("../../lib/admin-session.js");
21
+ vi.mock("../../lib/shop.js", () => ({
22
+ getHydrogenShop: () => "my-shop"
23
+ }));
24
+ const ADMIN_SESSION = {
25
+ token: "abc123",
26
+ storeFqdn: "my-shop"
27
+ };
28
+ describe("link", () => {
29
+ const outputMock = mockAndCaptureOutput();
30
+ beforeEach(async () => {
31
+ vi.mocked(getAdminSession).mockResolvedValue(ADMIN_SESSION);
32
+ vi.mocked(adminRequest).mockResolvedValue({
33
+ hydrogenStorefronts: [
34
+ {
35
+ id: "gid://shopify/HydrogenStorefront/1",
36
+ title: "Hydrogen",
37
+ productionUrl: "https://example.com"
38
+ }
39
+ ]
40
+ });
41
+ vi.mocked(getConfig).mockResolvedValue({});
42
+ });
43
+ afterEach(() => {
44
+ vi.resetAllMocks();
45
+ outputMock.clear();
46
+ });
47
+ it("makes a GraphQL call to fetch the storefronts", async () => {
48
+ await linkStorefront({});
49
+ expect(adminRequest).toHaveBeenCalledWith(
50
+ LinkStorefrontQuery,
51
+ ADMIN_SESSION
52
+ );
53
+ });
54
+ it("renders a list of choices and forwards the selection to setStorefront", async () => {
55
+ vi.mocked(renderSelectPrompt).mockResolvedValue(
56
+ "gid://shopify/HydrogenStorefront/1"
57
+ );
58
+ await linkStorefront({ path: "my-path" });
59
+ expect(setStorefront).toHaveBeenCalledWith("my-path", {
60
+ id: "gid://shopify/HydrogenStorefront/1",
61
+ title: "Hydrogen",
62
+ productionUrl: "https://example.com"
63
+ });
64
+ });
65
+ describe("when there are no Hydrogen storefronts", () => {
66
+ it("renders a message and returns early", async () => {
67
+ vi.mocked(adminRequest).mockResolvedValue({
68
+ hydrogenStorefronts: []
69
+ });
70
+ await linkStorefront({});
71
+ expect(outputMock.info()).toMatch(
72
+ /There are no Hydrogen storefronts on your Shop/g
73
+ );
74
+ expect(renderSelectPrompt).not.toHaveBeenCalled();
75
+ expect(setStorefront).not.toHaveBeenCalled();
76
+ });
77
+ });
78
+ describe("when no storefront gets selected", () => {
79
+ it("does not call setStorefront", async () => {
80
+ vi.mocked(renderSelectPrompt).mockResolvedValue("");
81
+ await linkStorefront({});
82
+ expect(setStorefront).not.toHaveBeenCalled();
83
+ });
84
+ });
85
+ describe("when a linked storefront already exists", () => {
86
+ beforeEach(() => {
87
+ vi.mocked(getConfig).mockResolvedValue({
88
+ storefront: {
89
+ id: "gid://shopify/HydrogenStorefront/2",
90
+ title: "Existing Link"
91
+ }
92
+ });
93
+ });
94
+ it("prompts the user to confirm", async () => {
95
+ vi.mocked(renderConfirmationPrompt).mockResolvedValue(true);
96
+ await linkStorefront({});
97
+ expect(renderConfirmationPrompt).toHaveBeenCalledWith({
98
+ message: expect.stringMatching(
99
+ /Do you want to link to a different Hydrogen storefront on Shopify\?/
100
+ )
101
+ });
102
+ });
103
+ describe("and the user cancels", () => {
104
+ it("returns early", async () => {
105
+ vi.mocked(renderConfirmationPrompt).mockResolvedValue(false);
106
+ await linkStorefront({});
107
+ expect(adminRequest).not.toHaveBeenCalled();
108
+ expect(setStorefront).not.toHaveBeenCalled();
109
+ });
110
+ });
111
+ describe("and the --force flag is provided", () => {
112
+ it("does not prompt the user to confirm", async () => {
113
+ await linkStorefront({ force: true });
114
+ expect(renderConfirmationPrompt).not.toHaveBeenCalled();
115
+ });
116
+ });
117
+ });
118
+ describe("when the --storefront flag is provided", () => {
119
+ it("does not prompt the user to make a selection", async () => {
120
+ await linkStorefront({ path: "my-path", storefront: "Hydrogen" });
121
+ expect(renderSelectPrompt).not.toHaveBeenCalled();
122
+ expect(setStorefront).toHaveBeenCalledWith("my-path", {
123
+ id: "gid://shopify/HydrogenStorefront/1",
124
+ title: "Hydrogen",
125
+ productionUrl: "https://example.com"
126
+ });
127
+ });
128
+ describe("and there is no matching storefront", () => {
129
+ it("renders a warning message and returns early", async () => {
130
+ const outputMock2 = mockAndCaptureOutput();
131
+ await linkStorefront({ storefront: "Does not exist" });
132
+ expect(setStorefront).not.toHaveBeenCalled();
133
+ expect(outputMock2.warn()).toMatch(/Couldn\'t find Does not exist/g);
134
+ });
135
+ });
136
+ });
137
+ });
@@ -0,0 +1,21 @@
1
+ import * as _oclif_core_lib_interfaces_parser_js from '@oclif/core/lib/interfaces/parser.js';
2
+ import Command from '@shopify/cli-kit/node/base-command';
3
+ import { Deployment } from '../../lib/graphql/admin/list-storefronts.js';
4
+
5
+ declare class List extends Command {
6
+ static description: string;
7
+ static hidden: boolean;
8
+ static flags: {
9
+ path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
10
+ shop: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
11
+ };
12
+ run(): Promise<void>;
13
+ }
14
+ interface Flags {
15
+ path?: string;
16
+ shop?: string;
17
+ }
18
+ declare function listStorefronts({ path, shop: flagShop }: Flags): Promise<void>;
19
+ declare function formatDeployment(deployment: Deployment | null): string;
20
+
21
+ export { List as default, formatDeployment, listStorefronts };