@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.
- package/dist/commands/hydrogen/codegen-unstable.d.ts +14 -0
- package/dist/commands/hydrogen/codegen-unstable.js +64 -0
- package/dist/commands/hydrogen/dev.d.ts +5 -0
- package/dist/commands/hydrogen/dev.js +48 -6
- package/dist/commands/hydrogen/env/list.d.ts +19 -0
- package/dist/commands/hydrogen/env/list.js +96 -0
- package/dist/commands/hydrogen/env/list.test.d.ts +1 -0
- package/dist/commands/hydrogen/env/list.test.js +151 -0
- package/dist/commands/hydrogen/env/pull.d.ts +23 -0
- package/dist/commands/hydrogen/env/pull.js +68 -0
- package/dist/commands/hydrogen/env/pull.test.d.ts +1 -0
- package/dist/commands/hydrogen/env/pull.test.js +112 -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/codegen.d.ts +25 -0
- package/dist/lib/codegen.js +128 -0
- package/dist/lib/colors.d.ts +3 -0
- package/dist/lib/colors.js +4 -1
- package/dist/lib/combined-environment-variables.d.ts +8 -0
- package/dist/lib/combined-environment-variables.js +74 -0
- package/dist/lib/combined-environment-variables.test.d.ts +1 -0
- package/dist/lib/combined-environment-variables.test.js +111 -0
- package/dist/lib/flags.d.ts +2 -0
- package/dist/lib/flags.js +13 -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-environments.d.ts +20 -0
- package/dist/lib/graphql/admin/list-environments.js +18 -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/mini-oxygen.d.ts +4 -1
- package/dist/lib/mini-oxygen.js +7 -3
- package/dist/lib/missing-storefronts.d.ts +5 -0
- package/dist/lib/missing-storefronts.js +18 -0
- package/dist/lib/pull-environment-variables.d.ts +19 -0
- package/dist/lib/pull-environment-variables.js +67 -0
- package/dist/lib/pull-environment-variables.test.d.ts +1 -0
- package/dist/lib/pull-environment-variables.test.js +174 -0
- package/dist/lib/render-errors.d.ts +16 -0
- package/dist/lib/render-errors.js +37 -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 +7 -5
|
@@ -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 };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import Command from '@shopify/cli-kit/node/base-command';
|
|
2
|
+
import { renderTable } from '@shopify/cli-kit/node/ui';
|
|
3
|
+
import { outputInfo, outputContent } from '@shopify/cli-kit/node/output';
|
|
4
|
+
import { adminRequest, parseGid } from '../../lib/graphql.js';
|
|
5
|
+
import { commonFlags } from '../../lib/flags.js';
|
|
6
|
+
import { getHydrogenShop } from '../../lib/shop.js';
|
|
7
|
+
import { getAdminSession } from '../../lib/admin-session.js';
|
|
8
|
+
import { ListStorefrontsQuery } from '../../lib/graphql/admin/list-storefronts.js';
|
|
9
|
+
import { logMissingStorefronts } from '../../lib/missing-storefronts.js';
|
|
10
|
+
|
|
11
|
+
class List extends Command {
|
|
12
|
+
static description = "Returns a list of Hydrogen storefronts available on a given shop.";
|
|
13
|
+
static hidden = true;
|
|
14
|
+
static flags = {
|
|
15
|
+
path: commonFlags.path,
|
|
16
|
+
shop: commonFlags.shop
|
|
17
|
+
};
|
|
18
|
+
async run() {
|
|
19
|
+
const { flags } = await this.parse(List);
|
|
20
|
+
await listStorefronts(flags);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function listStorefronts({ path, shop: flagShop }) {
|
|
24
|
+
const shop = await getHydrogenShop({ path, shop: flagShop });
|
|
25
|
+
const adminSession = await getAdminSession(shop);
|
|
26
|
+
const result = await adminRequest(
|
|
27
|
+
ListStorefrontsQuery,
|
|
28
|
+
adminSession
|
|
29
|
+
);
|
|
30
|
+
const storefrontsCount = result.hydrogenStorefronts.length;
|
|
31
|
+
if (storefrontsCount > 0) {
|
|
32
|
+
outputInfo(
|
|
33
|
+
outputContent`Found ${storefrontsCount.toString()} Hydrogen storefronts on ${shop}:\n`.value
|
|
34
|
+
);
|
|
35
|
+
const rows = result.hydrogenStorefronts.map(
|
|
36
|
+
({ id, title, productionUrl, currentProductionDeployment }) => ({
|
|
37
|
+
id: parseGid(id),
|
|
38
|
+
title,
|
|
39
|
+
productionUrl,
|
|
40
|
+
currentDeployment: formatDeployment(currentProductionDeployment)
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
renderTable({
|
|
44
|
+
rows,
|
|
45
|
+
columns: {
|
|
46
|
+
id: {
|
|
47
|
+
header: "ID"
|
|
48
|
+
},
|
|
49
|
+
title: {
|
|
50
|
+
header: "Name",
|
|
51
|
+
color: "whiteBright"
|
|
52
|
+
},
|
|
53
|
+
productionUrl: {
|
|
54
|
+
header: "Production URL"
|
|
55
|
+
},
|
|
56
|
+
currentDeployment: {
|
|
57
|
+
header: "Current deployment"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
} else {
|
|
62
|
+
logMissingStorefronts(adminSession);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const dateFormat = new Intl.DateTimeFormat("default", {
|
|
66
|
+
year: "numeric",
|
|
67
|
+
month: "long",
|
|
68
|
+
day: "numeric"
|
|
69
|
+
});
|
|
70
|
+
function formatDeployment(deployment) {
|
|
71
|
+
let message = "";
|
|
72
|
+
if (!deployment) {
|
|
73
|
+
return message;
|
|
74
|
+
}
|
|
75
|
+
message += dateFormat.format(new Date(deployment.createdAt));
|
|
76
|
+
if (deployment.commitMessage) {
|
|
77
|
+
const title = deployment.commitMessage.split(/\n/)[0];
|
|
78
|
+
message += `, ${title}`;
|
|
79
|
+
}
|
|
80
|
+
return message;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { List as default, formatDeployment, listStorefronts };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { vi, describe, beforeEach, afterEach, it, expect } from 'vitest';
|
|
2
|
+
import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output';
|
|
3
|
+
import { ListStorefrontsQuery } from '../../lib/graphql/admin/list-storefronts.js';
|
|
4
|
+
import { getAdminSession } from '../../lib/admin-session.js';
|
|
5
|
+
import { adminRequest } from '../../lib/graphql.js';
|
|
6
|
+
import { listStorefronts, formatDeployment } from './list.js';
|
|
7
|
+
|
|
8
|
+
vi.mock("../../lib/admin-session.js");
|
|
9
|
+
vi.mock("../../lib/graphql.js", async () => {
|
|
10
|
+
const original = await vi.importActual(
|
|
11
|
+
"../../lib/graphql.js"
|
|
12
|
+
);
|
|
13
|
+
return {
|
|
14
|
+
...original,
|
|
15
|
+
adminRequest: vi.fn()
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
vi.mock("../../lib/shop.js", () => ({
|
|
19
|
+
getHydrogenShop: () => "my-shop"
|
|
20
|
+
}));
|
|
21
|
+
describe("list", () => {
|
|
22
|
+
const ADMIN_SESSION = {
|
|
23
|
+
token: "abc123",
|
|
24
|
+
storeFqdn: "my-shop"
|
|
25
|
+
};
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
vi.mocked(getAdminSession).mockResolvedValue(ADMIN_SESSION);
|
|
28
|
+
vi.mocked(adminRequest).mockResolvedValue({
|
|
29
|
+
hydrogenStorefronts: []
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
vi.resetAllMocks();
|
|
34
|
+
mockAndCaptureOutput().clear();
|
|
35
|
+
});
|
|
36
|
+
it("makes a GraphQL call to fetch the storefronts", async () => {
|
|
37
|
+
await listStorefronts({});
|
|
38
|
+
expect(adminRequest).toHaveBeenCalledWith(
|
|
39
|
+
ListStorefrontsQuery,
|
|
40
|
+
ADMIN_SESSION
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
describe("and there are storefronts", () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
vi.mocked(adminRequest).mockResolvedValue({
|
|
46
|
+
hydrogenStorefronts: [
|
|
47
|
+
{
|
|
48
|
+
id: "gid://shopify/HydrogenStorefront/1",
|
|
49
|
+
title: "Hydrogen",
|
|
50
|
+
productionUrl: "https://example.com",
|
|
51
|
+
currentProductionDeployment: null
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "gid://shopify/HydrogenStorefront/2",
|
|
55
|
+
title: "Demo Store",
|
|
56
|
+
productionUrl: "https://demo.example.com",
|
|
57
|
+
currentProductionDeployment: {
|
|
58
|
+
id: "gid://shopify/HydrogenStorefrontDeployment/1",
|
|
59
|
+
createdAt: "2023-03-22T22:28:38Z",
|
|
60
|
+
commitMessage: "Update README.md"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
it("renders a list of storefronts", async () => {
|
|
67
|
+
const outputMock = mockAndCaptureOutput();
|
|
68
|
+
await listStorefronts({});
|
|
69
|
+
expect(outputMock.info()).toMatch(
|
|
70
|
+
/Found 2 Hydrogen storefronts on my-shop/g
|
|
71
|
+
);
|
|
72
|
+
expect(outputMock.info()).toMatch(
|
|
73
|
+
/1 Hydrogen https:\/\/example.com/g
|
|
74
|
+
);
|
|
75
|
+
expect(outputMock.info()).toMatch(
|
|
76
|
+
/2 Demo Store https:\/\/demo.example.com March 22, 2023, Update README.md/g
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe("and there are no storefronts", () => {
|
|
81
|
+
it("prompts the user to create a storefront", async () => {
|
|
82
|
+
const outputMock = mockAndCaptureOutput();
|
|
83
|
+
await listStorefronts({});
|
|
84
|
+
expect(outputMock.info()).toMatch(
|
|
85
|
+
/There are no Hydrogen storefronts on your Shop\./g
|
|
86
|
+
);
|
|
87
|
+
expect(outputMock.info()).toMatch(/Create a new Hydrogen storefront/g);
|
|
88
|
+
expect(outputMock.info()).toMatch(
|
|
89
|
+
/https:\/\/my\-shop\/admin\/custom_storefronts\/new/g
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe("formatDeployment", () => {
|
|
95
|
+
const createdAt = "2023-03-22T22:28:38Z";
|
|
96
|
+
it("returns a string combined with a date and commit message", () => {
|
|
97
|
+
const deployment = {
|
|
98
|
+
id: "gid://shopify/HydrogenStorefrontDeployment/1",
|
|
99
|
+
createdAt,
|
|
100
|
+
commitMessage: "Update README.md\n\nThis is a description of why the change was made."
|
|
101
|
+
};
|
|
102
|
+
expect(formatDeployment(deployment)).toStrictEqual(
|
|
103
|
+
"March 22, 2023, Update README.md"
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
describe("when there is no commit message", () => {
|
|
107
|
+
it("only returns the date", () => {
|
|
108
|
+
const deployment = {
|
|
109
|
+
id: "gid://shopify/HydrogenStorefrontDeployment/1",
|
|
110
|
+
createdAt,
|
|
111
|
+
commitMessage: null
|
|
112
|
+
};
|
|
113
|
+
expect(formatDeployment(deployment)).toStrictEqual("March 22, 2023");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
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 Unlink 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
|
+
};
|
|
10
|
+
run(): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
interface LinkFlags {
|
|
13
|
+
path?: string;
|
|
14
|
+
}
|
|
15
|
+
declare function unlinkStorefront({ path }: LinkFlags): Promise<void>;
|
|
16
|
+
|
|
17
|
+
export { LinkFlags, Unlink as default, unlinkStorefront };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import Command from '@shopify/cli-kit/node/base-command';
|
|
2
|
+
import { outputWarn, outputSuccess } from '@shopify/cli-kit/node/output';
|
|
3
|
+
import { commonFlags } from '../../lib/flags.js';
|
|
4
|
+
import { getConfig, unsetStorefront } from '../../lib/shopify-config.js';
|
|
5
|
+
|
|
6
|
+
class Unlink extends Command {
|
|
7
|
+
static description = "Unlink a local project from a Hydrogen storefront.";
|
|
8
|
+
static hidden = true;
|
|
9
|
+
static flags = {
|
|
10
|
+
path: commonFlags.path
|
|
11
|
+
};
|
|
12
|
+
async run() {
|
|
13
|
+
const { flags } = await this.parse(Unlink);
|
|
14
|
+
await unlinkStorefront(flags);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function unlinkStorefront({ path }) {
|
|
18
|
+
const actualPath = path ?? process.cwd();
|
|
19
|
+
const { storefront: configStorefront } = await getConfig(actualPath);
|
|
20
|
+
if (!configStorefront) {
|
|
21
|
+
outputWarn("This project isn't linked to a Hydrogen storefront.");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const storefrontTitle = configStorefront.title;
|
|
25
|
+
await unsetStorefront(actualPath);
|
|
26
|
+
outputSuccess(`You are no longer linked to ${storefrontTitle}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { Unlink as default, unlinkStorefront };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { vi, describe, afterEach, it, expect } from 'vitest';
|
|
2
|
+
import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output';
|
|
3
|
+
import { getConfig, unsetStorefront } from '../../lib/shopify-config.js';
|
|
4
|
+
import { unlinkStorefront } from './unlink.js';
|
|
5
|
+
|
|
6
|
+
vi.mock("../../lib/shopify-config.js");
|
|
7
|
+
describe("link", () => {
|
|
8
|
+
const outputMock = mockAndCaptureOutput();
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.resetAllMocks();
|
|
11
|
+
outputMock.clear();
|
|
12
|
+
});
|
|
13
|
+
it("removes the storefront information from the config file", async () => {
|
|
14
|
+
vi.mocked(getConfig).mockResolvedValue({
|
|
15
|
+
storefront: {
|
|
16
|
+
id: "gid://shopify/HydrogenStorefront/2",
|
|
17
|
+
title: "Existing Link"
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
await unlinkStorefront({ path: "my-path" });
|
|
21
|
+
expect(unsetStorefront).toHaveBeenCalledWith("my-path");
|
|
22
|
+
expect(outputMock.output()).toMatch(
|
|
23
|
+
/You are no longer linked to Existing Link/g
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
describe("when there is no existing storefront link", () => {
|
|
27
|
+
it("renders a warning message and returns early", async () => {
|
|
28
|
+
vi.mocked(getConfig).mockResolvedValue({});
|
|
29
|
+
await unlinkStorefront({});
|
|
30
|
+
expect(outputMock.output()).toMatch(
|
|
31
|
+
/This project isn\'t linked to a Hydrogen storefront\./g
|
|
32
|
+
);
|
|
33
|
+
expect(unsetStorefront).not.toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|