@shopify/cli-hydrogen 4.1.1 → 4.2.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 (75) hide show
  1. package/dist/commands/hydrogen/codegen-unstable.d.ts +14 -0
  2. package/dist/commands/hydrogen/codegen-unstable.js +64 -0
  3. package/dist/commands/hydrogen/dev.d.ts +5 -0
  4. package/dist/commands/hydrogen/dev.js +48 -6
  5. package/dist/commands/hydrogen/env/list.d.ts +19 -0
  6. package/dist/commands/hydrogen/env/list.js +96 -0
  7. package/dist/commands/hydrogen/env/list.test.d.ts +1 -0
  8. package/dist/commands/hydrogen/env/list.test.js +151 -0
  9. package/dist/commands/hydrogen/env/pull.d.ts +23 -0
  10. package/dist/commands/hydrogen/env/pull.js +68 -0
  11. package/dist/commands/hydrogen/env/pull.test.d.ts +1 -0
  12. package/dist/commands/hydrogen/env/pull.test.js +112 -0
  13. package/dist/commands/hydrogen/init.js +3 -1
  14. package/dist/commands/hydrogen/link.d.ts +24 -0
  15. package/dist/commands/hydrogen/link.js +102 -0
  16. package/dist/commands/hydrogen/link.test.d.ts +1 -0
  17. package/dist/commands/hydrogen/link.test.js +137 -0
  18. package/dist/commands/hydrogen/list.d.ts +21 -0
  19. package/dist/commands/hydrogen/list.js +83 -0
  20. package/dist/commands/hydrogen/list.test.d.ts +1 -0
  21. package/dist/commands/hydrogen/list.test.js +116 -0
  22. package/dist/commands/hydrogen/unlink.d.ts +17 -0
  23. package/dist/commands/hydrogen/unlink.js +29 -0
  24. package/dist/commands/hydrogen/unlink.test.d.ts +1 -0
  25. package/dist/commands/hydrogen/unlink.test.js +36 -0
  26. package/dist/generator-templates/routes/[robots.txt].tsx +111 -19
  27. package/dist/generator-templates/routes/collections/index.tsx +102 -0
  28. package/dist/lib/admin-session.d.ts +5 -0
  29. package/dist/lib/admin-session.js +16 -0
  30. package/dist/lib/admin-session.test.d.ts +1 -0
  31. package/dist/lib/admin-session.test.js +27 -0
  32. package/dist/lib/admin-urls.d.ts +8 -0
  33. package/dist/lib/admin-urls.js +18 -0
  34. package/dist/lib/codegen.d.ts +25 -0
  35. package/dist/lib/codegen.js +128 -0
  36. package/dist/lib/colors.d.ts +3 -0
  37. package/dist/lib/colors.js +4 -1
  38. package/dist/lib/combined-environment-variables.d.ts +8 -0
  39. package/dist/lib/combined-environment-variables.js +74 -0
  40. package/dist/lib/combined-environment-variables.test.d.ts +1 -0
  41. package/dist/lib/combined-environment-variables.test.js +111 -0
  42. package/dist/lib/flags.d.ts +2 -0
  43. package/dist/lib/flags.js +13 -0
  44. package/dist/lib/graphql/admin/link-storefront.d.ts +11 -0
  45. package/dist/lib/graphql/admin/link-storefront.js +11 -0
  46. package/dist/lib/graphql/admin/list-environments.d.ts +20 -0
  47. package/dist/lib/graphql/admin/list-environments.js +18 -0
  48. package/dist/lib/graphql/admin/list-storefronts.d.ts +17 -0
  49. package/dist/lib/graphql/admin/list-storefronts.js +16 -0
  50. package/dist/lib/graphql/admin/pull-variables.d.ts +16 -0
  51. package/dist/lib/graphql/admin/pull-variables.js +15 -0
  52. package/dist/lib/graphql.d.ts +21 -0
  53. package/dist/lib/graphql.js +18 -0
  54. package/dist/lib/graphql.test.d.ts +1 -0
  55. package/dist/lib/graphql.test.js +15 -0
  56. package/dist/lib/mini-oxygen.d.ts +4 -1
  57. package/dist/lib/mini-oxygen.js +7 -3
  58. package/dist/lib/missing-storefronts.d.ts +5 -0
  59. package/dist/lib/missing-storefronts.js +18 -0
  60. package/dist/lib/pull-environment-variables.d.ts +19 -0
  61. package/dist/lib/pull-environment-variables.js +67 -0
  62. package/dist/lib/pull-environment-variables.test.d.ts +1 -0
  63. package/dist/lib/pull-environment-variables.test.js +174 -0
  64. package/dist/lib/render-errors.d.ts +16 -0
  65. package/dist/lib/render-errors.js +37 -0
  66. package/dist/lib/shop.d.ts +7 -0
  67. package/dist/lib/shop.js +32 -0
  68. package/dist/lib/shop.test.d.ts +1 -0
  69. package/dist/lib/shop.test.js +78 -0
  70. package/dist/lib/shopify-config.d.ts +35 -0
  71. package/dist/lib/shopify-config.js +86 -0
  72. package/dist/lib/shopify-config.test.d.ts +1 -0
  73. package/dist/lib/shopify-config.test.js +209 -0
  74. package/oclif.manifest.json +1 -1
  75. package/package.json +7 -5
@@ -0,0 +1,111 @@
1
+ import { vi, describe, beforeEach, afterEach, test, expect } from 'vitest';
2
+ import { inTemporaryDirectory, writeFile } from '@shopify/cli-kit/node/fs';
3
+ import { joinPath } from '@shopify/cli-kit/node/path';
4
+ import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output';
5
+ import { combinedEnvironmentVariables } from './combined-environment-variables.js';
6
+ import { pullRemoteEnvironmentVariables } from './pull-environment-variables.js';
7
+ import { getConfig } from './shopify-config.js';
8
+
9
+ vi.mock("./shopify-config.js");
10
+ vi.mock("./pull-environment-variables.js");
11
+ describe("combinedEnvironmentVariables()", () => {
12
+ beforeEach(() => {
13
+ vi.mocked(getConfig).mockResolvedValue({
14
+ storefront: {
15
+ id: "gid://shopify/HydrogenStorefront/1",
16
+ title: "Hydrogen"
17
+ }
18
+ });
19
+ vi.mocked(pullRemoteEnvironmentVariables).mockResolvedValue([
20
+ {
21
+ id: "gid://shopify/HydrogenStorefrontEnvironmentVariable/1",
22
+ key: "PUBLIC_API_TOKEN",
23
+ value: "abc123",
24
+ isSecret: false
25
+ }
26
+ ]);
27
+ });
28
+ afterEach(() => {
29
+ vi.resetAllMocks();
30
+ mockAndCaptureOutput().clear();
31
+ });
32
+ test("calls pullRemoteEnvironmentVariables", async () => {
33
+ await inTemporaryDirectory(async (tmpDir) => {
34
+ await combinedEnvironmentVariables({
35
+ envBranch: "main",
36
+ root: tmpDir,
37
+ shop: "my-shop"
38
+ });
39
+ expect(pullRemoteEnvironmentVariables).toHaveBeenCalledWith({
40
+ envBranch: "main",
41
+ root: tmpDir,
42
+ flagShop: "my-shop",
43
+ silent: true
44
+ });
45
+ });
46
+ });
47
+ test("renders a message about injection", async () => {
48
+ await inTemporaryDirectory(async (tmpDir) => {
49
+ const outputMock = mockAndCaptureOutput();
50
+ await combinedEnvironmentVariables({ root: tmpDir, shop: "my-shop" });
51
+ expect(outputMock.info()).toMatch(
52
+ /Injecting environment variables into MiniOxygen/
53
+ );
54
+ });
55
+ });
56
+ test("lists all of the variables being used", async () => {
57
+ await inTemporaryDirectory(async (tmpDir) => {
58
+ const outputMock = mockAndCaptureOutput();
59
+ await combinedEnvironmentVariables({ root: tmpDir, shop: "my-shop" });
60
+ expect(outputMock.info()).toMatch(/Using PUBLIC_API_TOKEN from Hydrogen/);
61
+ });
62
+ });
63
+ describe("when one of the variables is a secret", () => {
64
+ beforeEach(() => {
65
+ vi.mocked(pullRemoteEnvironmentVariables).mockResolvedValue([
66
+ {
67
+ id: "gid://shopify/HydrogenStorefrontEnvironmentVariable/1",
68
+ key: "PUBLIC_API_TOKEN",
69
+ value: "",
70
+ isSecret: true
71
+ }
72
+ ]);
73
+ });
74
+ test("uses special messaging to alert the user", async () => {
75
+ await inTemporaryDirectory(async (tmpDir) => {
76
+ const outputMock = mockAndCaptureOutput();
77
+ await combinedEnvironmentVariables({ root: tmpDir, shop: "my-shop" });
78
+ expect(outputMock.info()).toMatch(
79
+ /Ignoring PUBLIC_API_TOKEN \(value is marked as secret\)/
80
+ );
81
+ });
82
+ });
83
+ });
84
+ describe("when there are local variables", () => {
85
+ test("includes local variables in the list", async () => {
86
+ await inTemporaryDirectory(async (tmpDir) => {
87
+ const filePath = joinPath(tmpDir, ".env");
88
+ await writeFile(filePath, "LOCAL_TOKEN=1");
89
+ const outputMock = mockAndCaptureOutput();
90
+ await combinedEnvironmentVariables({ root: tmpDir });
91
+ expect(outputMock.info()).toMatch(/Using LOCAL_TOKEN from \.env/);
92
+ });
93
+ });
94
+ describe("and they overwrite remote variables", () => {
95
+ test("uses special messaging to alert the user", async () => {
96
+ await inTemporaryDirectory(async (tmpDir) => {
97
+ const filePath = joinPath(tmpDir, ".env");
98
+ await writeFile(filePath, "PUBLIC_API_TOKEN=abc");
99
+ const outputMock = mockAndCaptureOutput();
100
+ await combinedEnvironmentVariables({ root: tmpDir, shop: "my-shop" });
101
+ expect(outputMock.info()).toMatch(
102
+ /Ignoring PUBLIC_API_TOKEN \(overwritten via \.env\)/
103
+ );
104
+ expect(outputMock.info()).toMatch(
105
+ /Using PUBLIC_API_TOKEN from \.env/
106
+ );
107
+ });
108
+ });
109
+ });
110
+ });
111
+ });
@@ -4,6 +4,8 @@ declare const commonFlags: {
4
4
  path: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
5
5
  port: _oclif_core_lib_interfaces_parser_js.OptionFlag<number, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
6
6
  force: _oclif_core_lib_interfaces_parser_js.BooleanFlag<boolean>;
7
+ shop: _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
8
+ "env-branch": _oclif_core_lib_interfaces_parser_js.OptionFlag<string | undefined, _oclif_core_lib_interfaces_parser_js.CustomOptions>;
7
9
  };
8
10
  declare function flagsToCamelObject(obj: Record<string, any>): any;
9
11
  /**
package/dist/lib/flags.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { camelize } from '@shopify/cli-kit/common/string';
3
3
  import { renderInfo } from '@shopify/cli-kit/node/ui';
4
+ import { normalizeStoreFqdn } from '@shopify/cli-kit/node/context/fqdn';
4
5
  import { colors } from './colors.js';
5
6
 
6
7
  const commonFlags = {
@@ -17,6 +18,18 @@ const commonFlags = {
17
18
  description: "Overwrite the destination directory and files if they already exist.",
18
19
  env: "SHOPIFY_HYDROGEN_FLAG_FORCE",
19
20
  char: "f"
21
+ }),
22
+ shop: Flags.string({
23
+ char: "s",
24
+ 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).",
25
+ env: "SHOPIFY_SHOP",
26
+ parse: async (input) => normalizeStoreFqdn(input)
27
+ }),
28
+ ["env-branch"]: Flags.string({
29
+ description: "Specify an environment's branch name when using remote environment variables.",
30
+ env: "SHOPIFY_HYDROGEN_ENVIRONMENT_BRANCH",
31
+ char: "e",
32
+ hidden: true
20
33
  })
21
34
  };
22
35
  function flagsToCamelObject(obj) {
@@ -0,0 +1,11 @@
1
+ declare const LinkStorefrontQuery = "#graphql\n query LinkStorefront {\n hydrogenStorefronts {\n id\n title\n productionUrl\n }\n }\n";
2
+ interface HydrogenStorefront {
3
+ id: string;
4
+ title: string;
5
+ productionUrl: string;
6
+ }
7
+ interface LinkStorefrontSchema {
8
+ hydrogenStorefronts: HydrogenStorefront[];
9
+ }
10
+
11
+ export { LinkStorefrontQuery, LinkStorefrontSchema };
@@ -0,0 +1,11 @@
1
+ const LinkStorefrontQuery = `#graphql
2
+ query LinkStorefront {
3
+ hydrogenStorefronts {
4
+ id
5
+ title
6
+ productionUrl
7
+ }
8
+ }
9
+ `;
10
+
11
+ export { LinkStorefrontQuery };
@@ -0,0 +1,20 @@
1
+ declare const ListEnvironmentsQuery = "#graphql\n query ListStorefronts($id: ID!) {\n hydrogenStorefront(id: $id) {\n id\n productionUrl\n environments {\n branch\n createdAt\n id\n name\n type\n url\n }\n }\n }\n";
2
+ type EnvironmentType = 'PREVIEW' | 'PRODUCTION' | 'CUSTOM';
3
+ interface Environment {
4
+ branch: string | null;
5
+ createdAt: string;
6
+ id: string;
7
+ name: string;
8
+ type: EnvironmentType;
9
+ url: string | null;
10
+ }
11
+ interface HydrogenStorefront {
12
+ id: string;
13
+ environments: Environment[];
14
+ productionUrl: string;
15
+ }
16
+ interface ListEnvironmentsSchema {
17
+ hydrogenStorefront: HydrogenStorefront | null;
18
+ }
19
+
20
+ export { Environment, EnvironmentType, ListEnvironmentsQuery, ListEnvironmentsSchema };
@@ -0,0 +1,18 @@
1
+ const ListEnvironmentsQuery = `#graphql
2
+ query ListStorefronts($id: ID!) {
3
+ hydrogenStorefront(id: $id) {
4
+ id
5
+ productionUrl
6
+ environments {
7
+ branch
8
+ createdAt
9
+ id
10
+ name
11
+ type
12
+ url
13
+ }
14
+ }
15
+ }
16
+ `;
17
+
18
+ export { ListEnvironmentsQuery };
@@ -0,0 +1,17 @@
1
+ declare const ListStorefrontsQuery = "#graphql\n query ListStorefronts {\n hydrogenStorefronts {\n id\n title\n productionUrl\n currentProductionDeployment {\n id\n createdAt\n commitMessage\n }\n }\n }\n";
2
+ interface Deployment {
3
+ id: string;
4
+ createdAt: string;
5
+ commitMessage: string | null;
6
+ }
7
+ interface HydrogenStorefront {
8
+ id: string;
9
+ title: string;
10
+ productionUrl?: string;
11
+ currentProductionDeployment: Deployment | null;
12
+ }
13
+ interface ListStorefrontsSchema {
14
+ hydrogenStorefronts: HydrogenStorefront[];
15
+ }
16
+
17
+ export { Deployment, ListStorefrontsQuery, ListStorefrontsSchema };
@@ -0,0 +1,16 @@
1
+ const ListStorefrontsQuery = `#graphql
2
+ query ListStorefronts {
3
+ hydrogenStorefronts {
4
+ id
5
+ title
6
+ productionUrl
7
+ currentProductionDeployment {
8
+ id
9
+ createdAt
10
+ commitMessage
11
+ }
12
+ }
13
+ }
14
+ `;
15
+
16
+ export { ListStorefrontsQuery };
@@ -0,0 +1,16 @@
1
+ declare const PullVariablesQuery = "#graphql\n query PullVariables($id: ID!, $branch: String) {\n hydrogenStorefront(id: $id) {\n id\n environmentVariables(branchName: $branch) {\n id\n isSecret\n key\n value\n }\n }\n }\n";
2
+ interface EnvironmentVariable {
3
+ id: string;
4
+ isSecret: boolean;
5
+ key: string;
6
+ value: string;
7
+ }
8
+ interface HydrogenStorefront {
9
+ id: string;
10
+ environmentVariables: EnvironmentVariable[];
11
+ }
12
+ interface PullVariablesSchema {
13
+ hydrogenStorefront: HydrogenStorefront | null;
14
+ }
15
+
16
+ export { EnvironmentVariable, PullVariablesQuery, PullVariablesSchema };
@@ -0,0 +1,15 @@
1
+ const PullVariablesQuery = `#graphql
2
+ query PullVariables($id: ID!, $branch: String) {
3
+ hydrogenStorefront(id: $id) {
4
+ id
5
+ environmentVariables(branchName: $branch) {
6
+ id
7
+ isSecret
8
+ key
9
+ value
10
+ }
11
+ }
12
+ }
13
+ `;
14
+
15
+ export { PullVariablesQuery };
@@ -0,0 +1,21 @@
1
+ import { GraphQLVariables } from '@shopify/cli-kit/node/api/graphql';
2
+ import { AdminSession } from '@shopify/cli-kit/node/session';
3
+
4
+ /**
5
+ * This is a temporary workaround until cli-kit includes a way to specify
6
+ * API versions for the Admin API because we need to target the unstable
7
+ * branch for this early access phase.
8
+ *
9
+ * @param query - GraphQL query to execute.
10
+ * @param session - Shopify admin session including token and Store FQDN.
11
+ * @param variables - GraphQL variables to pass to the query.
12
+ * @returns The response of the query of generic type <T>.
13
+ */
14
+ declare function adminRequest<T>(query: string, session: AdminSession, variables?: GraphQLVariables): Promise<T>;
15
+ /**
16
+ * @param gid a Global ID to parse (e.g. 'gid://shopify/HydrogenStorefront/1')
17
+ * @returns the ID of the record (e.g. '1')
18
+ */
19
+ declare function parseGid(gid: string): string;
20
+
21
+ export { adminRequest, parseGid };
@@ -0,0 +1,18 @@
1
+ import { graphqlRequest } from '@shopify/cli-kit/node/api/graphql';
2
+ import { AbortError } from '@shopify/cli-kit/node/error';
3
+
4
+ async function adminRequest(query, session, variables) {
5
+ const api = "Admin";
6
+ const url = `https://${session.storeFqdn}/admin/api/unstable/graphql.json`;
7
+ return graphqlRequest({ query, api, url, token: session.token, variables });
8
+ }
9
+ const GID_REGEXP = /gid:\/\/shopify\/\w*\/(\d+)/;
10
+ function parseGid(gid) {
11
+ const matches = GID_REGEXP.exec(gid);
12
+ if (matches && matches[1] !== void 0) {
13
+ return matches[1];
14
+ }
15
+ throw new AbortError(`Invalid Global ID: ${gid}`);
16
+ }
17
+
18
+ export { adminRequest, parseGid };
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,15 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { AbortError } from '@shopify/cli-kit/node/error';
3
+ import { parseGid } from './graphql.js';
4
+
5
+ describe("parseGid", () => {
6
+ it("returns an ID", () => {
7
+ const id = parseGid("gid://shopify/HydrogenStorefront/324");
8
+ expect(id).toStrictEqual("324");
9
+ });
10
+ describe("when the global ID is invalid", () => {
11
+ it("throws an error", () => {
12
+ expect(() => parseGid("321asd")).toThrow(AbortError);
13
+ });
14
+ });
15
+ });
@@ -4,8 +4,11 @@ type MiniOxygenOptions = {
4
4
  watch?: boolean;
5
5
  buildPathClient: string;
6
6
  buildPathWorkerFile: string;
7
+ environmentVariables?: {
8
+ [key: string]: string;
9
+ };
7
10
  };
8
- declare function startMiniOxygen({ root, port, watch, buildPathWorkerFile, buildPathClient, }: MiniOxygenOptions): Promise<void>;
11
+ declare function startMiniOxygen({ root, port, watch, buildPathWorkerFile, buildPathClient, environmentVariables, }: MiniOxygenOptions): Promise<void>;
9
12
  declare function logResponse(request: Request, response: Response): void;
10
13
 
11
14
  export { logResponse, startMiniOxygen };
@@ -8,7 +8,8 @@ async function startMiniOxygen({
8
8
  port = 3e3,
9
9
  watch = false,
10
10
  buildPathWorkerFile,
11
- buildPathClient
11
+ buildPathClient,
12
+ environmentVariables = {}
12
13
  }) {
13
14
  const { default: miniOxygen } = await import('@shopify/mini-oxygen');
14
15
  const miniOxygenPreview = miniOxygen.default ?? miniOxygen;
@@ -21,8 +22,11 @@ async function startMiniOxygen({
21
22
  watch,
22
23
  autoReload: watch,
23
24
  modules: true,
24
- env: process.env,
25
- envPath: await fileExists(dotenvPath) ? dotenvPath : void 0,
25
+ env: {
26
+ ...environmentVariables,
27
+ ...process.env
28
+ },
29
+ envPath: !Object.keys(environmentVariables).length && await fileExists(dotenvPath) ? dotenvPath : void 0,
26
30
  log: () => {
27
31
  },
28
32
  buildWatchPaths: watch ? [resolvePath(root, buildPathWorkerFile)] : void 0,
@@ -0,0 +1,5 @@
1
+ import { AdminSession } from '@shopify/cli-kit/node/session';
2
+
3
+ declare function logMissingStorefronts(adminSession: AdminSession): void;
4
+
5
+ export { logMissingStorefronts };
@@ -0,0 +1,18 @@
1
+ import { renderInfo } from '@shopify/cli-kit/node/ui';
2
+ import { newHydrogenStorefrontUrl } from './admin-urls.js';
3
+
4
+ function logMissingStorefronts(adminSession) {
5
+ renderInfo({
6
+ headline: "Hydrogen storefronts",
7
+ body: "There are no Hydrogen storefronts on your Shop.",
8
+ nextSteps: [
9
+ `Ensure you have specified the correct shop (you specified: ${adminSession.storeFqdn})`,
10
+ `Ensure you have the Hydrogen sales channel installed https://apps.shopify.com/hydrogen`,
11
+ `Create a new Hydrogen storefront: ${newHydrogenStorefrontUrl(
12
+ adminSession
13
+ )}`
14
+ ]
15
+ });
16
+ }
17
+
18
+ export { logMissingStorefronts };
@@ -0,0 +1,19 @@
1
+ import { EnvironmentVariable } from './graphql/admin/pull-variables.js';
2
+
3
+ interface Arguments {
4
+ envBranch?: string;
5
+ root: string;
6
+ /**
7
+ * Optional shop override that developers would have passed using the --shop
8
+ * flag.
9
+ */
10
+ flagShop?: string;
11
+ /**
12
+ * Does not prompt the user to fix any errors that are encountered (e.g. no
13
+ * linked storefront)
14
+ */
15
+ silent?: boolean;
16
+ }
17
+ declare function pullRemoteEnvironmentVariables({ envBranch, root, flagShop, silent, }: Arguments): Promise<EnvironmentVariable[]>;
18
+
19
+ export { pullRemoteEnvironmentVariables };
@@ -0,0 +1,67 @@
1
+ import { renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
2
+ import { outputContent, outputToken, outputInfo } from '@shopify/cli-kit/node/output';
3
+ import { linkStorefront } from '../commands/hydrogen/link.js';
4
+ import { adminRequest } from './graphql.js';
5
+ import { getHydrogenShop } from './shop.js';
6
+ import { getAdminSession } from './admin-session.js';
7
+ import { getConfig } from './shopify-config.js';
8
+ import { renderMissingLink, renderMissingStorefront } from './render-errors.js';
9
+ import { PullVariablesQuery } from './graphql/admin/pull-variables.js';
10
+
11
+ async function pullRemoteEnvironmentVariables({
12
+ envBranch,
13
+ root,
14
+ flagShop,
15
+ silent
16
+ }) {
17
+ const shop = await getHydrogenShop({ path: root, shop: flagShop });
18
+ const adminSession = await getAdminSession(shop);
19
+ let configStorefront = (await getConfig(root)).storefront;
20
+ if (!configStorefront?.id) {
21
+ if (!silent) {
22
+ renderMissingLink({ adminSession });
23
+ const runLink = await renderConfirmationPrompt({
24
+ message: outputContent`Run ${outputToken.genericShellCommand(
25
+ `npx shopify hydrogen link`
26
+ )}?`.value
27
+ });
28
+ if (!runLink) {
29
+ return [];
30
+ }
31
+ await linkStorefront({ path: root, shop: flagShop, silent });
32
+ }
33
+ }
34
+ configStorefront = (await getConfig(root)).storefront;
35
+ if (!configStorefront) {
36
+ return [];
37
+ }
38
+ if (!silent) {
39
+ outputInfo(
40
+ `Fetching environment variables from ${configStorefront.title}...`
41
+ );
42
+ }
43
+ const result = await adminRequest(
44
+ PullVariablesQuery,
45
+ adminSession,
46
+ {
47
+ id: configStorefront.id,
48
+ branch: envBranch
49
+ }
50
+ );
51
+ const storefront = result.hydrogenStorefront;
52
+ if (!storefront) {
53
+ if (!silent) {
54
+ renderMissingStorefront({ adminSession, storefront: configStorefront });
55
+ }
56
+ return [];
57
+ }
58
+ if (!storefront.environmentVariables.length) {
59
+ if (!silent) {
60
+ outputInfo(`No environment variables found.`);
61
+ }
62
+ return [];
63
+ }
64
+ return storefront.environmentVariables;
65
+ }
66
+
67
+ export { pullRemoteEnvironmentVariables };
@@ -0,0 +1,174 @@
1
+ import { vi, describe, beforeEach, afterEach, it, expect } from 'vitest';
2
+ import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output';
3
+ import { inTemporaryDirectory } from '@shopify/cli-kit/node/fs';
4
+ import { renderConfirmationPrompt } from '@shopify/cli-kit/node/ui';
5
+ import { PullVariablesQuery } from './graphql/admin/pull-variables.js';
6
+ import { getAdminSession } from './admin-session.js';
7
+ import { adminRequest } from './graphql.js';
8
+ import { getConfig } from './shopify-config.js';
9
+ import { renderMissingLink, renderMissingStorefront } from './render-errors.js';
10
+ import { linkStorefront } from '../commands/hydrogen/link.js';
11
+ import { pullRemoteEnvironmentVariables } from './pull-environment-variables.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("../commands/hydrogen/link.js");
21
+ vi.mock("./admin-session.js");
22
+ vi.mock("./shopify-config.js");
23
+ vi.mock("./render-errors.js");
24
+ vi.mock("./graphql.js", async () => {
25
+ const original = await vi.importActual(
26
+ "./graphql.js"
27
+ );
28
+ return {
29
+ ...original,
30
+ adminRequest: vi.fn()
31
+ };
32
+ });
33
+ vi.mock("./shop.js", () => ({
34
+ getHydrogenShop: () => "my-shop"
35
+ }));
36
+ describe("pullRemoteEnvironmentVariables", () => {
37
+ const ENVIRONMENT_VARIABLES_RESPONSE = [
38
+ {
39
+ id: "gid://shopify/HydrogenStorefrontEnvironmentVariable/1",
40
+ key: "PUBLIC_API_TOKEN",
41
+ value: "abc123",
42
+ isSecret: false
43
+ }
44
+ ];
45
+ const ADMIN_SESSION = {
46
+ token: "abc123",
47
+ storeFqdn: "my-shop"
48
+ };
49
+ beforeEach(async () => {
50
+ vi.mocked(getAdminSession).mockResolvedValue(ADMIN_SESSION);
51
+ vi.mocked(getConfig).mockResolvedValue({
52
+ storefront: {
53
+ id: "gid://shopify/HydrogenStorefront/2",
54
+ title: "Existing Link"
55
+ }
56
+ });
57
+ vi.mocked(adminRequest).mockResolvedValue({
58
+ hydrogenStorefront: {
59
+ id: "gid://shopify/HydrogenStorefront/1",
60
+ environmentVariables: ENVIRONMENT_VARIABLES_RESPONSE
61
+ }
62
+ });
63
+ });
64
+ afterEach(() => {
65
+ vi.resetAllMocks();
66
+ mockAndCaptureOutput().clear();
67
+ });
68
+ it("makes a GraphQL call to fetch environment variables", async () => {
69
+ await inTemporaryDirectory(async (tmpDir) => {
70
+ await pullRemoteEnvironmentVariables({
71
+ root: tmpDir,
72
+ envBranch: "staging"
73
+ });
74
+ expect(adminRequest).toHaveBeenCalledWith(
75
+ PullVariablesQuery,
76
+ ADMIN_SESSION,
77
+ {
78
+ id: "gid://shopify/HydrogenStorefront/2",
79
+ branch: "staging"
80
+ }
81
+ );
82
+ });
83
+ });
84
+ it("returns environment variables", async () => {
85
+ await inTemporaryDirectory(async (tmpDir) => {
86
+ const environmentVariables = await pullRemoteEnvironmentVariables({
87
+ root: tmpDir
88
+ });
89
+ expect(environmentVariables).toBe(ENVIRONMENT_VARIABLES_RESPONSE);
90
+ });
91
+ });
92
+ describe("when environment variables are empty", () => {
93
+ beforeEach(() => {
94
+ vi.mocked(adminRequest).mockResolvedValue({
95
+ hydrogenStorefront: {
96
+ id: "gid://shopify/HydrogenStorefront/1",
97
+ environmentVariables: []
98
+ }
99
+ });
100
+ });
101
+ it("renders a message", async () => {
102
+ await inTemporaryDirectory(async (tmpDir) => {
103
+ const outputMock = mockAndCaptureOutput();
104
+ await pullRemoteEnvironmentVariables({ root: tmpDir });
105
+ expect(outputMock.info()).toMatch(/No environment variables found\./);
106
+ });
107
+ });
108
+ it("returns an empty array", async () => {
109
+ await inTemporaryDirectory(async (tmpDir) => {
110
+ const environmentVariables = await pullRemoteEnvironmentVariables({
111
+ root: tmpDir
112
+ });
113
+ expect(environmentVariables).toStrictEqual([]);
114
+ });
115
+ });
116
+ });
117
+ describe("when there is no linked storefront", () => {
118
+ beforeEach(() => {
119
+ vi.mocked(getConfig).mockResolvedValue({
120
+ storefront: void 0
121
+ });
122
+ });
123
+ it("calls renderMissingLink", async () => {
124
+ await inTemporaryDirectory(async (tmpDir) => {
125
+ await pullRemoteEnvironmentVariables({ root: tmpDir });
126
+ expect(renderMissingLink).toHaveBeenCalledOnce();
127
+ });
128
+ });
129
+ it("prompts the user to create a link", async () => {
130
+ vi.mocked(renderConfirmationPrompt).mockResolvedValue(true);
131
+ await inTemporaryDirectory(async (tmpDir) => {
132
+ await pullRemoteEnvironmentVariables({ root: tmpDir });
133
+ expect(renderConfirmationPrompt).toHaveBeenCalledWith({
134
+ message: expect.stringMatching(/Run .*npx shopify hydrogen link.*\?/)
135
+ });
136
+ expect(linkStorefront).toHaveBeenCalledWith({
137
+ path: tmpDir
138
+ });
139
+ });
140
+ });
141
+ describe("and the user does not create a new link", () => {
142
+ it("returns an empty array", async () => {
143
+ vi.mocked(renderConfirmationPrompt).mockResolvedValue(false);
144
+ await inTemporaryDirectory(async (tmpDir) => {
145
+ const environmentVariables = await pullRemoteEnvironmentVariables({
146
+ root: tmpDir
147
+ });
148
+ expect(environmentVariables).toStrictEqual([]);
149
+ });
150
+ });
151
+ });
152
+ });
153
+ describe("when there is no matching storefront in the shop", () => {
154
+ beforeEach(() => {
155
+ vi.mocked(adminRequest).mockResolvedValue({
156
+ hydrogenStorefront: null
157
+ });
158
+ });
159
+ it("calls renderMissingStorefront", async () => {
160
+ await inTemporaryDirectory(async (tmpDir) => {
161
+ await pullRemoteEnvironmentVariables({ root: tmpDir });
162
+ expect(renderMissingStorefront).toHaveBeenCalledOnce();
163
+ });
164
+ });
165
+ it("returns an empty array", async () => {
166
+ await inTemporaryDirectory(async (tmpDir) => {
167
+ const environmentVariables = await pullRemoteEnvironmentVariables({
168
+ root: tmpDir
169
+ });
170
+ expect(environmentVariables).toStrictEqual([]);
171
+ });
172
+ });
173
+ });
174
+ });