@shopify/cli-hydrogen 4.2.0 → 5.0.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 (54) hide show
  1. package/dist/commands/hydrogen/build.js +6 -5
  2. package/dist/commands/hydrogen/check.js +1 -1
  3. package/dist/commands/hydrogen/codegen-unstable.d.ts +1 -0
  4. package/dist/commands/hydrogen/codegen-unstable.js +6 -0
  5. package/dist/commands/hydrogen/env/list.d.ts +2 -3
  6. package/dist/commands/hydrogen/env/list.js +42 -44
  7. package/dist/commands/hydrogen/env/list.test.js +18 -24
  8. package/dist/commands/hydrogen/env/pull.d.ts +2 -3
  9. package/dist/commands/hydrogen/env/pull.js +42 -23
  10. package/dist/commands/hydrogen/env/pull.test.js +16 -4
  11. package/dist/commands/hydrogen/init.js +3 -13
  12. package/dist/commands/hydrogen/link.d.ts +0 -1
  13. package/dist/commands/hydrogen/link.js +34 -36
  14. package/dist/commands/hydrogen/link.test.js +43 -27
  15. package/dist/commands/hydrogen/list.d.ts +2 -2
  16. package/dist/commands/hydrogen/list.js +43 -39
  17. package/dist/commands/hydrogen/list.test.js +24 -32
  18. package/dist/commands/hydrogen/shortcut.js +6 -7
  19. package/dist/commands/hydrogen/shortcut.test.js +8 -9
  20. package/dist/commands/hydrogen/unlink.d.ts +0 -1
  21. package/dist/commands/hydrogen/unlink.js +5 -3
  22. package/dist/lib/admin-session.d.ts +1 -0
  23. package/dist/lib/codegen.d.ts +3 -2
  24. package/dist/lib/codegen.js +20 -7
  25. package/dist/lib/combined-environment-variables.js +19 -36
  26. package/dist/lib/combined-environment-variables.test.js +7 -7
  27. package/dist/lib/config.d.ts +1 -1
  28. package/dist/lib/config.js +67 -63
  29. package/dist/lib/flags.js +2 -3
  30. package/dist/lib/graphql/admin/link-storefront.d.ts +12 -9
  31. package/dist/lib/graphql/admin/link-storefront.js +18 -1
  32. package/dist/lib/graphql/admin/list-environments.d.ts +6 -5
  33. package/dist/lib/graphql/admin/list-environments.js +11 -1
  34. package/dist/lib/graphql/admin/list-storefronts.d.ts +13 -5
  35. package/dist/lib/graphql/admin/list-storefronts.js +18 -1
  36. package/dist/lib/graphql/admin/pull-variables.d.ts +6 -1
  37. package/dist/lib/graphql/admin/pull-variables.js +14 -1
  38. package/dist/lib/mini-oxygen.js +1 -1
  39. package/dist/lib/process.d.ts +6 -0
  40. package/dist/lib/process.js +17 -0
  41. package/dist/lib/pull-environment-variables.d.ts +1 -0
  42. package/dist/lib/pull-environment-variables.js +4 -14
  43. package/dist/lib/remix-version-interop.js +1 -1
  44. package/dist/lib/shell.d.ts +5 -6
  45. package/dist/lib/shell.js +65 -17
  46. package/dist/lib/shell.test.d.ts +1 -0
  47. package/dist/lib/shell.test.js +85 -0
  48. package/dist/lib/shopify-config.d.ts +1 -1
  49. package/dist/lib/shopify-config.js +2 -2
  50. package/dist/lib/transpile-ts.js +6 -0
  51. package/oclif.manifest.json +1 -1
  52. package/package.json +7 -6
  53. package/dist/lib/colors.d.ts +0 -11
  54. package/dist/lib/colors.js +0 -11
@@ -1,14 +1,14 @@
1
1
  import path from 'path';
2
+ import { Flags } from '@oclif/core';
3
+ import Command from '@shopify/cli-kit/node/base-command';
2
4
  import { outputInfo, outputContent, outputToken, outputWarn } from '@shopify/cli-kit/node/output';
3
5
  import { rmdir, fileSize, copyFile } from '@shopify/cli-kit/node/fs';
6
+ import { getPackageManager } from '@shopify/cli-kit/node/node-package-manager';
7
+ import colors from '@shopify/cli-kit/node/colors';
4
8
  import { getProjectPaths, getRemixConfig } from '../../lib/config.js';
5
9
  import { commonFlags, deprecated, flagsToCamelObject } from '../../lib/flags.js';
6
- import Command from '@shopify/cli-kit/node/base-command';
7
- import { Flags } from '@oclif/core';
8
10
  import { checkLockfileStatus } from '../../lib/check-lockfile.js';
9
11
  import { findMissingRoutes } from '../../lib/missing-routes.js';
10
- import { getPackageManager } from '@shopify/cli-kit/node/node-package-manager';
11
- import { colors } from '../../lib/colors.js';
12
12
 
13
13
  const LOG_WORKER_BUILT = "\u{1F4E6} Worker built";
14
14
  class Build extends Command {
@@ -18,7 +18,8 @@ class Build extends Command {
18
18
  sourcemap: Flags.boolean({
19
19
  description: "Generate sourcemaps for the build.",
20
20
  env: "SHOPIFY_HYDROGEN_FLAG_SOURCEMAP",
21
- default: true
21
+ default: true,
22
+ allowNo: true
22
23
  }),
23
24
  ["disable-route-warning"]: Flags.boolean({
24
25
  description: "Disable warning about missing standard routes.",
@@ -29,7 +29,7 @@ class GenerateRoute extends Command {
29
29
  }
30
30
  }
31
31
  async function runCheckRoutes({ directory }) {
32
- const remixConfig = await getRemixConfig(directory);
32
+ const remixConfig = await getRemixConfig(directory, true);
33
33
  logMissingRoutes(findMissingRoutes(remixConfig));
34
34
  }
35
35
 
@@ -6,6 +6,7 @@ declare class Codegen extends Command {
6
6
  static flags: {
7
7
  path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
8
8
  "codegen-config-path": _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
9
+ "force-sfapi-version": _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
9
10
  watch: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
10
11
  };
11
12
  run(): Promise<void>;
@@ -15,6 +15,10 @@ class Codegen extends Command {
15
15
  description: "Specify a path to a codegen configuration file. Defaults to `<root>/codegen.ts` if it exists.",
16
16
  required: false
17
17
  }),
18
+ ["force-sfapi-version"]: Flags.string({
19
+ description: "Force generating Storefront API types for a specific version instead of using the one provided in Hydrogen. A token can also be provided with this format: `<version>:<token>`.",
20
+ hidden: true
21
+ }),
18
22
  watch: Flags.boolean({
19
23
  description: "Watch the project for changes to update types on file save.",
20
24
  required: false,
@@ -33,6 +37,7 @@ class Codegen extends Command {
33
37
  async function runCodegen({
34
38
  path: appPath,
35
39
  codegenConfigPath,
40
+ forceSfapiVersion,
36
41
  watch
37
42
  }) {
38
43
  const { root } = getProjectPaths(appPath);
@@ -42,6 +47,7 @@ async function runCodegen({
42
47
  const generatedFiles = await generateTypes({
43
48
  ...remixConfig,
44
49
  configFilePath: codegenConfigPath,
50
+ forceSfapiVersion,
45
51
  watch
46
52
  });
47
53
  if (!watch) {
@@ -1,9 +1,8 @@
1
1
  import * as _oclif_core_lib_interfaces_parser_js from '@oclif/core/lib/interfaces/parser.js';
2
2
  import Command from '@shopify/cli-kit/node/base-command';
3
3
 
4
- declare class List extends Command {
4
+ declare class EnvList extends Command {
5
5
  static description: string;
6
- static hidden: boolean;
7
6
  static flags: {
8
7
  path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
9
8
  shop: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
@@ -16,4 +15,4 @@ interface Flags {
16
15
  }
17
16
  declare function listEnvironments({ path, shop: flagShop }: Flags): Promise<void>;
18
17
 
19
- export { List as default, listEnvironments };
18
+ export { EnvList as default, listEnvironments };
@@ -1,24 +1,24 @@
1
1
  import Command from '@shopify/cli-kit/node/base-command';
2
- import { renderConfirmationPrompt, renderTable } from '@shopify/cli-kit/node/ui';
3
- import { outputContent, outputToken, outputNewline } from '@shopify/cli-kit/node/output';
2
+ import { renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
3
+ import { pluralize } from '@shopify/cli-kit/common/string';
4
+ import colors from '@shopify/cli-kit/node/colors';
5
+ import { outputContent, outputToken, outputNewline, outputInfo } from '@shopify/cli-kit/node/output';
4
6
  import { linkStorefront } from '../link.js';
5
- import { adminRequest } from '../../../lib/graphql.js';
6
7
  import { commonFlags } from '../../../lib/flags.js';
7
8
  import { getHydrogenShop } from '../../../lib/shop.js';
8
9
  import { getAdminSession } from '../../../lib/admin-session.js';
9
- import { ListEnvironmentsQuery } from '../../../lib/graphql/admin/list-environments.js';
10
+ import { getStorefrontEnvironments } from '../../../lib/graphql/admin/list-environments.js';
10
11
  import { getConfig } from '../../../lib/shopify-config.js';
11
12
  import { renderMissingLink, renderMissingStorefront } from '../../../lib/render-errors.js';
12
13
 
13
- class List extends Command {
14
- static description = "List the environments on your Hydrogen storefront.";
15
- static hidden = true;
14
+ class EnvList extends Command {
15
+ static description = "List the environments on your linked Hydrogen storefront.";
16
16
  static flags = {
17
17
  path: commonFlags.path,
18
18
  shop: commonFlags.shop
19
19
  };
20
20
  async run() {
21
- const { flags } = await this.parse(List);
21
+ const { flags } = await this.parse(EnvList);
22
22
  await listEnvironments(flags);
23
23
  }
24
24
  }
@@ -43,54 +43,52 @@ async function listEnvironments({ path, shop: flagShop }) {
43
43
  if (!configStorefront) {
44
44
  return;
45
45
  }
46
- const result = await adminRequest(
47
- ListEnvironmentsQuery,
46
+ const { storefront } = await getStorefrontEnvironments(
48
47
  adminSession,
49
- {
50
- id: configStorefront.id
51
- }
48
+ configStorefront.id
52
49
  );
53
- const hydrogenStorefront = result.hydrogenStorefront;
54
- if (!hydrogenStorefront) {
50
+ if (!storefront) {
55
51
  renderMissingStorefront({ adminSession, storefront: configStorefront });
56
52
  return;
57
53
  }
58
- const previewEnvironmentIndex = hydrogenStorefront.environments.findIndex(
54
+ const previewEnvironmentIndex = storefront.environments.findIndex(
59
55
  (env) => env.type === "PREVIEW"
60
56
  );
61
- const previewEnvironment = hydrogenStorefront.environments.splice(
57
+ const previewEnvironment = storefront.environments.splice(
62
58
  previewEnvironmentIndex,
63
59
  1
64
60
  );
65
- hydrogenStorefront.environments.push(previewEnvironment[0]);
66
- const rows = hydrogenStorefront.environments.map(
67
- ({ branch, name, url, type }) => {
68
- const environmentUrl = type === "PRODUCTION" ? hydrogenStorefront.productionUrl : url;
69
- return {
70
- name,
71
- branch: branch ? branch : "-",
72
- url: environmentUrl ? environmentUrl : "-"
73
- };
74
- }
75
- );
61
+ storefront.environments.push(previewEnvironment[0]);
76
62
  outputNewline();
77
- renderTable({
78
- rows,
79
- columns: {
80
- name: {
81
- header: "Name",
82
- color: "whiteBright"
83
- },
84
- branch: {
85
- header: "Branch",
86
- color: "yellow"
87
- },
88
- url: {
89
- header: "URL",
90
- color: "green"
91
- }
63
+ outputInfo(
64
+ pluralizedEnvironments({
65
+ environments: storefront.environments,
66
+ storefrontTitle: configStorefront.title
67
+ }).toString()
68
+ );
69
+ storefront.environments.forEach(({ name, branch, type, url }) => {
70
+ outputNewline();
71
+ const environmentUrl = type === "PRODUCTION" ? storefront.productionUrl : url;
72
+ outputInfo(
73
+ outputContent`${colors.whiteBright(name)}${branch ? ` ${colors.dim(`(Branch: ${branch})`)}` : ""}`.value
74
+ );
75
+ if (environmentUrl) {
76
+ outputInfo(
77
+ outputContent` ${colors.whiteBright(environmentUrl)}`.value
78
+ );
92
79
  }
93
80
  });
94
81
  }
82
+ const pluralizedEnvironments = ({
83
+ environments,
84
+ storefrontTitle
85
+ }) => {
86
+ return pluralize(
87
+ environments,
88
+ (environments2) => `Showing ${environments2.length} environments for the Hydrogen storefront ${storefrontTitle}`,
89
+ (_environment) => `Showing 1 environment for the Hydrogen storefront ${storefrontTitle}`,
90
+ () => `There are no environments for the Hydrogen storefront ${storefrontTitle}`
91
+ );
92
+ };
95
93
 
96
- export { List as default, listEnvironments };
94
+ export { EnvList as default, listEnvironments };
@@ -2,14 +2,14 @@ import { vi, describe, beforeEach, afterEach, it, expect } from 'vitest';
2
2
  import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output';
3
3
  import { inTemporaryDirectory } from '@shopify/cli-kit/node/fs';
4
4
  import { renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
5
- import { ListEnvironmentsQuery } from '../../../lib/graphql/admin/list-environments.js';
5
+ import { getStorefrontEnvironments } from '../../../lib/graphql/admin/list-environments.js';
6
6
  import { getAdminSession } from '../../../lib/admin-session.js';
7
- import { adminRequest } from '../../../lib/graphql.js';
8
7
  import { getConfig } from '../../../lib/shopify-config.js';
9
8
  import { renderMissingLink, renderMissingStorefront } from '../../../lib/render-errors.js';
10
9
  import { linkStorefront } from '../link.js';
11
10
  import { listEnvironments } from './list.js';
12
11
 
12
+ const SHOP = "my-shop";
13
13
  vi.mock("@shopify/cli-kit/node/ui", async () => {
14
14
  const original = await vi.importActual("@shopify/cli-kit/node/ui");
15
15
  return {
@@ -21,20 +21,16 @@ vi.mock("../link.js");
21
21
  vi.mock("../../../lib/admin-session.js");
22
22
  vi.mock("../../../lib/shopify-config.js");
23
23
  vi.mock("../../../lib/render-errors.js");
24
- vi.mock("../../../lib/graphql.js", async () => {
25
- const original = await vi.importActual("../../../lib/graphql.js");
26
- return {
27
- ...original,
28
- adminRequest: vi.fn()
29
- };
24
+ vi.mock("../../../lib/graphql/admin/list-environments.js", () => {
25
+ return { getStorefrontEnvironments: vi.fn() };
30
26
  });
31
27
  vi.mock("../../../lib/shop.js", () => ({
32
- getHydrogenShop: () => "my-shop"
28
+ getHydrogenShop: () => SHOP
33
29
  }));
34
30
  describe("listEnvironments", () => {
35
31
  const ADMIN_SESSION = {
36
32
  token: "abc123",
37
- storeFqdn: "my-shop"
33
+ storeFqdn: SHOP
38
34
  };
39
35
  const PRODUCTION_ENVIRONMENT = {
40
36
  id: "gid://shopify/HydrogenStorefrontEnvironment/1",
@@ -68,8 +64,8 @@ describe("listEnvironments", () => {
68
64
  title: "Existing Link"
69
65
  }
70
66
  });
71
- vi.mocked(adminRequest).mockResolvedValue({
72
- hydrogenStorefront: {
67
+ vi.mocked(getStorefrontEnvironments).mockResolvedValue({
68
+ storefront: {
73
69
  id: "gid://shopify/HydrogenStorefront/1",
74
70
  productionUrl: "https://example.com",
75
71
  environments: [
@@ -87,12 +83,9 @@ describe("listEnvironments", () => {
87
83
  it("makes a GraphQL call to fetch environment variables", async () => {
88
84
  await inTemporaryDirectory(async (tmpDir) => {
89
85
  await listEnvironments({ path: tmpDir });
90
- expect(adminRequest).toHaveBeenCalledWith(
91
- ListEnvironmentsQuery,
86
+ expect(getStorefrontEnvironments).toHaveBeenCalledWith(
92
87
  ADMIN_SESSION,
93
- {
94
- id: "gid://shopify/HydrogenStorefront/1"
95
- }
88
+ "gid://shopify/HydrogenStorefront/1"
96
89
  );
97
90
  });
98
91
  });
@@ -101,12 +94,13 @@ describe("listEnvironments", () => {
101
94
  const output = mockAndCaptureOutput();
102
95
  await listEnvironments({ path: tmpDir });
103
96
  expect(output.info()).toMatch(
104
- /Production\s*main\s*https:\/\/example\.com/
105
- );
106
- expect(output.info()).toMatch(
107
- /Staging\s*staging\s*https:\/\/oxygen-456\.example\.com/
97
+ /Showing 3 environments for the Hydrogen storefront Existing Link/
108
98
  );
109
- expect(output.info()).toMatch(/Preview\s*-\s*-/);
99
+ expect(output.info()).toMatch(/Production \(Branch: main\)/);
100
+ expect(output.info()).toMatch(/https:\/\/example\.com/);
101
+ expect(output.info()).toMatch(/Staging \(Branch: staging\)/);
102
+ expect(output.info()).toMatch(/https:\/\/oxygen-456\.example\.com/);
103
+ expect(output.info()).toMatch(/Preview/);
110
104
  });
111
105
  });
112
106
  describe("when there is no linked storefront", () => {
@@ -137,8 +131,8 @@ describe("listEnvironments", () => {
137
131
  });
138
132
  describe("when there is no matching storefront in the shop", () => {
139
133
  beforeEach(() => {
140
- vi.mocked(adminRequest).mockResolvedValue({
141
- hydrogenStorefront: null
134
+ vi.mocked(getStorefrontEnvironments).mockResolvedValue({
135
+ storefront: null
142
136
  });
143
137
  });
144
138
  it("calls renderMissingStorefront", async () => {
@@ -1,9 +1,8 @@
1
1
  import * as _oclif_core_lib_interfaces_parser_js from '@oclif/core/lib/interfaces/parser.js';
2
2
  import Command from '@shopify/cli-kit/node/base-command';
3
3
 
4
- declare class Pull extends Command {
4
+ declare class EnvPull extends Command {
5
5
  static description: string;
6
- static hidden: boolean;
7
6
  static flags: {
8
7
  "env-branch": _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
9
8
  path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
@@ -20,4 +19,4 @@ interface Flags {
20
19
  }
21
20
  declare function pullVariables({ envBranch, force, path, shop: flagShop, }: Flags): Promise<void>;
22
21
 
23
- export { Pull as default, pullVariables };
22
+ export { EnvPull as default, pullVariables };
@@ -1,15 +1,17 @@
1
+ import { diffLines } from 'diff';
1
2
  import Command from '@shopify/cli-kit/node/base-command';
2
- import { renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
3
- import { outputWarn, outputSuccess } from '@shopify/cli-kit/node/output';
4
- import { fileExists, writeFile } from '@shopify/cli-kit/node/fs';
3
+ import { renderInfo, renderConfirmationPrompt, renderWarning, renderSuccess } from '@shopify/cli-kit/node/ui';
4
+ import { outputContent, outputToken } from '@shopify/cli-kit/node/output';
5
+ import { fileExists, readFile, writeFile } from '@shopify/cli-kit/node/fs';
5
6
  import { resolvePath } from '@shopify/cli-kit/node/path';
7
+ import { patchEnvFile } from '@shopify/cli-kit/node/dot-env';
8
+ import colors from '@shopify/cli-kit/node/colors';
6
9
  import { commonFlags, flagsToCamelObject } from '../../../lib/flags.js';
7
10
  import { pullRemoteEnvironmentVariables } from '../../../lib/pull-environment-variables.js';
8
11
  import { getConfig } from '../../../lib/shopify-config.js';
9
12
 
10
- class Pull extends Command {
13
+ class EnvPull extends Command {
11
14
  static description = "Populate your .env with variables from your Hydrogen storefront.";
12
- static hidden = true;
13
15
  static flags = {
14
16
  ["env-branch"]: commonFlags["env-branch"],
15
17
  path: commonFlags.path,
@@ -17,7 +19,7 @@ class Pull extends Command {
17
19
  force: commonFlags.force
18
20
  };
19
21
  async run() {
20
- const { flags } = await this.parse(Pull);
22
+ const { flags } = await this.parse(EnvPull);
21
23
  await pullVariables({ ...flagsToCamelObject(flags) });
22
24
  }
23
25
  }
@@ -36,33 +38,50 @@ async function pullVariables({
36
38
  if (!environmentVariables.length) {
37
39
  return;
38
40
  }
41
+ const fileName = colors.whiteBright(`.env`);
39
42
  const dotEnvPath = resolvePath(actualPath, ".env");
43
+ const fetchedEnv = {};
44
+ environmentVariables.forEach(({ isSecret, key, value }) => {
45
+ fetchedEnv[key] = isSecret ? `""` : value;
46
+ });
40
47
  if (await fileExists(dotEnvPath) && !force) {
48
+ const existingEnv = await readFile(dotEnvPath);
49
+ const patchedEnv = patchEnvFile(existingEnv, fetchedEnv);
50
+ if (existingEnv === patchedEnv) {
51
+ renderInfo({
52
+ body: `No changes to your ${fileName} file`
53
+ });
54
+ return;
55
+ }
56
+ const diff = diffLines(existingEnv, patchedEnv);
41
57
  const overwrite = await renderConfirmationPrompt({
42
- message: "Warning: .env file already exists. Do you want to overwrite it?"
58
+ confirmationMessage: `Yes, confirm changes`,
59
+ cancellationMessage: `No, make changes later`,
60
+ message: outputContent`We'll make the following changes to your .env file:
61
+
62
+ ${outputToken.linesDiff(diff)}
63
+ Continue?`.value
43
64
  });
44
65
  if (!overwrite) {
45
66
  return;
46
67
  }
68
+ await writeFile(dotEnvPath, patchedEnv);
69
+ } else {
70
+ const newEnv = patchEnvFile(null, fetchedEnv);
71
+ await writeFile(dotEnvPath, newEnv);
47
72
  }
48
- let hasSecretVariables = false;
49
- const contents = environmentVariables.map(({ key, value, isSecret }) => {
50
- let line = `${key}="${value}"`;
51
- if (isSecret) {
52
- hasSecretVariables = true;
53
- line = `# ${key} is marked as secret and its value is hidden
54
- ` + line;
55
- }
56
- return line;
57
- }).join("\n") + "\n";
73
+ const hasSecretVariables = environmentVariables.some(
74
+ ({ isSecret }) => isSecret
75
+ );
58
76
  if (hasSecretVariables) {
59
77
  const { storefront: configStorefront } = await getConfig(actualPath);
60
- outputWarn(
61
- `${configStorefront.title} contains environment variables marked as secret, so their values weren\u2019t pulled.`
62
- );
78
+ renderWarning({
79
+ body: `${configStorefront.title} contains environment variables marked as secret, so their values weren\u2019t pulled.`
80
+ });
63
81
  }
64
- await writeFile(dotEnvPath, contents);
65
- outputSuccess("Updated .env");
82
+ renderSuccess({
83
+ body: ["Changes have been made to your", { filePath: fileName }, "file"]
84
+ });
66
85
  }
67
86
 
68
- export { Pull as default, pullVariables };
87
+ export { EnvPull as default, pullVariables };
@@ -69,7 +69,7 @@ describe("pullVariables", () => {
69
69
  expect(await fileExists(filePath)).toBeFalsy();
70
70
  await pullVariables({ path: tmpDir });
71
71
  expect(await readFile(filePath)).toStrictEqual(
72
- 'PUBLIC_API_TOKEN="abc123"\n# PRIVATE_API_TOKEN is marked as secret and its value is hidden\nPRIVATE_API_TOKEN=""\n'
72
+ 'PUBLIC_API_TOKEN=abc123\nPRIVATE_API_TOKEN=""'
73
73
  );
74
74
  });
75
75
  });
@@ -77,8 +77,18 @@ describe("pullVariables", () => {
77
77
  await inTemporaryDirectory(async (tmpDir) => {
78
78
  const outputMock = mockAndCaptureOutput();
79
79
  await pullVariables({ path: tmpDir });
80
- expect(outputMock.warn()).toStrictEqual(
81
- "Existing Link contains environment variables marked as secret, so their values weren\u2019t pulled."
80
+ expect(outputMock.warn()).toMatch(
81
+ /Existing Link contains environment variables marked as secret, so their/
82
+ );
83
+ expect(outputMock.warn()).toMatch(/values weren’t pulled./);
84
+ });
85
+ });
86
+ it("renders a success message", async () => {
87
+ await inTemporaryDirectory(async (tmpDir) => {
88
+ const outputMock = mockAndCaptureOutput();
89
+ await pullVariables({ path: tmpDir });
90
+ expect(outputMock.info()).toMatch(
91
+ /Changes have been made to your \.env file/
82
92
  );
83
93
  });
84
94
  });
@@ -92,8 +102,10 @@ describe("pullVariables", () => {
92
102
  await writeFile(filePath, "EXISTING_TOKEN=1");
93
103
  await pullVariables({ path: tmpDir });
94
104
  expect(renderConfirmationPrompt).toHaveBeenCalledWith({
105
+ confirmationMessage: `Yes, confirm changes`,
106
+ cancellationMessage: `No, make changes later`,
95
107
  message: expect.stringMatching(
96
- /Warning: \.env file already exists\. Do you want to overwrite it\?/
108
+ /We'll make the following changes to your \.env file:/
97
109
  )
98
110
  });
99
111
  });
@@ -1,4 +1,6 @@
1
1
  import Command from '@shopify/cli-kit/node/base-command';
2
+ import { readdir } from 'node:fs/promises';
3
+ import { fileURLToPath } from 'node:url';
2
4
  import { packageManagerUsedForCreating, installNodeModules } from '@shopify/cli-kit/node/node-package-manager';
3
5
  import { renderFatalError, renderSelectPrompt, renderTextPrompt, renderConfirmationPrompt, renderInfo, renderTasks, renderSuccess } from '@shopify/cli-kit/node/ui';
4
6
  import { Flags } from '@oclif/core';
@@ -9,8 +11,7 @@ import { commonFlags, flagsToCamelObject, parseProcessFlags } from '../../lib/fl
9
11
  import { transpileProject } from '../../lib/transpile-ts.js';
10
12
  import { getLatestTemplates } from '../../lib/template-downloader.js';
11
13
  import { checkHydrogenVersion } from '../../lib/check-version.js';
12
- import { readdir } from 'fs/promises';
13
- import { fileURLToPath } from 'url';
14
+ import { supressNodeExperimentalWarnings } from '../../lib/process.js';
14
15
 
15
16
  const STARTER_TEMPLATES = ["hello-world", "demo-store"];
16
17
  const FLAG_MAP = { f: "force" };
@@ -167,16 +168,5 @@ async function runInit(options = parseProcessFlags(process.argv, FLAG_MAP)) {
167
168
  async function projectExists(projectDir) {
168
169
  return await fileExists(projectDir) && await isDirectory(projectDir) && (await readdir(projectDir)).length > 0;
169
170
  }
170
- function supressNodeExperimentalWarnings() {
171
- const warningListener = process.listeners("warning")[0];
172
- if (warningListener) {
173
- process.removeAllListeners("warning");
174
- process.prependListener("warning", (warning) => {
175
- if (warning.name != "ExperimentalWarning") {
176
- warningListener(warning);
177
- }
178
- });
179
- }
180
- }
181
171
 
182
172
  export { Init as default, runInit };
@@ -3,7 +3,6 @@ import Command from '@shopify/cli-kit/node/base-command';
3
3
 
4
4
  declare class Link extends Command {
5
5
  static description: string;
6
- static hidden: boolean;
7
6
  static flags: {
8
7
  force: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
9
8
  path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
@@ -1,25 +1,20 @@
1
1
  import { Flags } from '@oclif/core';
2
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';
3
+ import { renderConfirmationPrompt, renderWarning, renderSelectPrompt, renderSuccess } from '@shopify/cli-kit/node/ui';
6
4
  import { commonFlags } from '../../lib/flags.js';
7
5
  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';
6
+ import { getStorefronts } from '../../lib/graphql/admin/link-storefront.js';
11
7
  import { getConfig, setStorefront } from '../../lib/shopify-config.js';
12
8
  import { logMissingStorefronts } from '../../lib/missing-storefronts.js';
9
+ import { getCliCommand } from '../../lib/shell.js';
13
10
 
14
11
  class Link extends Command {
15
12
  static description = "Link a local project to one of your shop's Hydrogen storefronts.";
16
- static hidden = true;
17
13
  static flags = {
18
14
  force: commonFlags.force,
19
15
  path: commonFlags.path,
20
16
  shop: commonFlags.shop,
21
17
  storefront: Flags.string({
22
- char: "h",
23
18
  description: `The name of a Hydrogen Storefront (e.g. "Jane's Apparel")`,
24
19
  env: "SHOPIFY_HYDROGEN_STOREFRONT"
25
20
  })
@@ -46,56 +41,59 @@ async function linkStorefront({
46
41
  return;
47
42
  }
48
43
  }
49
- const adminSession = await getAdminSession(shop);
50
- const result = await adminRequest(
51
- LinkStorefrontQuery,
52
- adminSession
53
- );
54
- if (!result.hydrogenStorefronts.length) {
44
+ const { storefronts, adminSession } = await getStorefronts(shop);
45
+ if (storefronts.length === 0) {
55
46
  logMissingStorefronts(adminSession);
56
47
  return;
57
48
  }
58
49
  let selectedStorefront;
50
+ const cliCommand = await getCliCommand();
59
51
  if (flagStorefront) {
60
- selectedStorefront = result.hydrogenStorefronts.find(
61
- (storefront) => storefront.title === flagStorefront
52
+ selectedStorefront = storefronts.find(
53
+ ({ title }) => title === flagStorefront
62
54
  );
63
55
  if (!selectedStorefront) {
64
56
  renderWarning({
65
57
  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
58
+ body: [
59
+ "There's no storefront matching",
60
+ { userInput: flagStorefront },
61
+ "on your",
62
+ { userInput: shop },
63
+ "shop. To see all available Hydrogen storefronts, run",
64
+ {
65
+ command: `${cliCommand} list`
66
+ }
67
+ ]
69
68
  });
70
69
  return;
71
70
  }
72
71
  } else {
73
- const choices = result.hydrogenStorefronts.map((storefront) => ({
74
- label: `${storefront.title} ${storefront.productionUrl}${storefront.id === configStorefront?.id ? " (Current)" : ""}`,
75
- value: storefront.id
72
+ const choices = storefronts.map(({ id, title, productionUrl }) => ({
73
+ value: id,
74
+ label: `${title} (${productionUrl})`
76
75
  }));
77
76
  const storefrontId = await renderSelectPrompt({
78
- message: "Choose a Hydrogen storefront to link this project to:",
79
- choices,
80
- defaultValue: "true"
77
+ message: "Choose a Hydrogen storefront to link",
78
+ choices
81
79
  });
82
- selectedStorefront = result.hydrogenStorefronts.find(
83
- (storefront) => storefront.id === storefrontId
84
- );
80
+ selectedStorefront = storefronts.find(({ id }) => id === storefrontId);
85
81
  }
86
82
  if (!selectedStorefront) {
87
83
  return;
88
84
  }
89
85
  await setStorefront(path ?? process.cwd(), selectedStorefront);
90
- outputSuccess(`Linked to ${selectedStorefront.title}`);
91
86
  if (!silent) {
92
- outputInfo(
93
- `Admin URL: ${hydrogenStorefrontUrl(
94
- adminSession,
95
- parseGid(selectedStorefront.id)
96
- )}`
97
- );
98
- outputInfo(`Site URL: ${selectedStorefront.productionUrl}`);
87
+ renderSuccess({
88
+ body: [{ userInput: selectedStorefront.title }, "is now linked"],
89
+ nextSteps: [
90
+ [
91
+ "Run",
92
+ { command: `${cliCommand} dev` },
93
+ "to start your local development server and start building"
94
+ ]
95
+ ]
96
+ });
99
97
  }
100
98
  }
101
99