@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.
- package/dist/commands/hydrogen/dev.d.ts +1 -0
- package/dist/commands/hydrogen/dev.js +8 -0
- package/dist/commands/hydrogen/env/pull.d.ts +21 -0
- package/dist/commands/hydrogen/env/pull.js +115 -0
- package/dist/commands/hydrogen/env/pull.test.d.ts +1 -0
- package/dist/commands/hydrogen/env/pull.test.js +205 -0
- package/dist/commands/hydrogen/init.js +3 -1
- package/dist/commands/hydrogen/link.d.ts +24 -0
- package/dist/commands/hydrogen/link.js +102 -0
- package/dist/commands/hydrogen/link.test.d.ts +1 -0
- package/dist/commands/hydrogen/link.test.js +137 -0
- package/dist/commands/hydrogen/list.d.ts +21 -0
- package/dist/commands/hydrogen/list.js +83 -0
- package/dist/commands/hydrogen/list.test.d.ts +1 -0
- package/dist/commands/hydrogen/list.test.js +116 -0
- package/dist/commands/hydrogen/unlink.d.ts +17 -0
- package/dist/commands/hydrogen/unlink.js +29 -0
- package/dist/commands/hydrogen/unlink.test.d.ts +1 -0
- package/dist/commands/hydrogen/unlink.test.js +36 -0
- package/dist/generator-templates/routes/[robots.txt].tsx +111 -19
- package/dist/generator-templates/routes/collections/index.tsx +102 -0
- package/dist/lib/admin-session.d.ts +5 -0
- package/dist/lib/admin-session.js +16 -0
- package/dist/lib/admin-session.test.d.ts +1 -0
- package/dist/lib/admin-session.test.js +27 -0
- package/dist/lib/admin-urls.d.ts +8 -0
- package/dist/lib/admin-urls.js +18 -0
- package/dist/lib/flags.d.ts +1 -0
- package/dist/lib/flags.js +7 -0
- package/dist/lib/graphql/admin/link-storefront.d.ts +11 -0
- package/dist/lib/graphql/admin/link-storefront.js +11 -0
- package/dist/lib/graphql/admin/list-storefronts.d.ts +17 -0
- package/dist/lib/graphql/admin/list-storefronts.js +16 -0
- package/dist/lib/graphql/admin/pull-variables.d.ts +16 -0
- package/dist/lib/graphql/admin/pull-variables.js +15 -0
- package/dist/lib/graphql.d.ts +21 -0
- package/dist/lib/graphql.js +18 -0
- package/dist/lib/graphql.test.d.ts +1 -0
- package/dist/lib/graphql.test.js +15 -0
- package/dist/lib/missing-storefronts.d.ts +5 -0
- package/dist/lib/missing-storefronts.js +18 -0
- package/dist/lib/shop.d.ts +7 -0
- package/dist/lib/shop.js +32 -0
- package/dist/lib/shop.test.d.ts +1 -0
- package/dist/lib/shop.test.js +78 -0
- package/dist/lib/shopify-config.d.ts +35 -0
- package/dist/lib/shopify-config.js +86 -0
- package/dist/lib/shopify-config.test.d.ts +1 -0
- package/dist/lib/shopify-config.test.js +209 -0
- package/oclif.manifest.json +1 -1
- 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 @@
|
|
|
1
|
+
|
|
@@ -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
|
-
"
|
|
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 @@
|
|
|
1
|
+
|
|
@@ -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 };
|