@shopify/cli-hydrogen 4.1.0 → 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-routes.d.ts +3 -1
- package/dist/lib/missing-routes.js +7 -6
- package/dist/lib/missing-routes.test.d.ts +1 -0
- package/dist/lib/missing-routes.test.js +45 -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
|
@@ -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
|
+
});
|
|
@@ -3,19 +3,28 @@ import {
|
|
|
3
3
|
type ErrorBoundaryComponent,
|
|
4
4
|
} from '@shopify/remix-oxygen';
|
|
5
5
|
import {useCatch, useRouteError, isRouteErrorResponse} from '@remix-run/react';
|
|
6
|
+
import type {Shop} from '@shopify/hydrogen/storefront-api-types';
|
|
7
|
+
import {parseGid} from '@shopify/hydrogen';
|
|
6
8
|
|
|
7
|
-
export const loader = ({request}: LoaderArgs) => {
|
|
9
|
+
export const loader = async ({request, context}: LoaderArgs) => {
|
|
8
10
|
const url = new URL(request.url);
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
const {shop} = await context.storefront.query<{shop: Pick<Shop, 'id'>}>(
|
|
13
|
+
SHOP_QUERY,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
return new Response(
|
|
17
|
+
robotsTxtData({url: url.origin, shopId: parseGid(shop.id).id}),
|
|
18
|
+
{
|
|
19
|
+
status: 200,
|
|
20
|
+
headers: {
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
22
|
+
'Content-Type': 'text/plain',
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
24
|
+
'Cache-Control': `max-age=${60 * 60 * 24}`,
|
|
25
|
+
},
|
|
17
26
|
},
|
|
18
|
-
|
|
27
|
+
);
|
|
19
28
|
};
|
|
20
29
|
|
|
21
30
|
export const ErrorBoundaryV1: ErrorBoundaryComponent = ({error}) => {
|
|
@@ -48,28 +57,111 @@ export function ErrorBoundary() {
|
|
|
48
57
|
}
|
|
49
58
|
}
|
|
50
59
|
|
|
51
|
-
function robotsTxtData({url}: {url
|
|
60
|
+
function robotsTxtData({url, shopId}: {shopId?: string; url?: string}) {
|
|
52
61
|
const sitemapUrl = url ? `${url}/sitemap.xml` : undefined;
|
|
53
62
|
|
|
54
63
|
return `
|
|
55
64
|
User-agent: *
|
|
56
|
-
|
|
57
|
-
Disallow: /cart
|
|
58
|
-
Disallow: /orders
|
|
59
|
-
Disallow: /checkouts/
|
|
60
|
-
Disallow: /checkout
|
|
61
|
-
Disallow: /carts
|
|
62
|
-
Disallow: /account
|
|
63
|
-
${sitemapUrl ? `Sitemap: ${sitemapUrl}` : ''}
|
|
65
|
+
${generalDisallowRules({sitemapUrl, shopId})}
|
|
64
66
|
|
|
65
67
|
# Google adsbot ignores robots.txt unless specifically named!
|
|
66
68
|
User-agent: adsbot-google
|
|
67
69
|
Disallow: /checkouts/
|
|
68
70
|
Disallow: /checkout
|
|
69
71
|
Disallow: /carts
|
|
70
|
-
Disallow: /orders
|
|
72
|
+
Disallow: /orders${
|
|
73
|
+
shopId
|
|
74
|
+
? `
|
|
75
|
+
Disallow: /${shopId}/checkouts
|
|
76
|
+
Disallow: /${shopId}/orders`
|
|
77
|
+
: ''
|
|
78
|
+
}
|
|
79
|
+
Disallow: /*?*oseid=*
|
|
80
|
+
Disallow: /*preview_theme_id*
|
|
81
|
+
Disallow: /*preview_script_id*
|
|
82
|
+
|
|
83
|
+
User-agent: Nutch
|
|
84
|
+
Disallow: /
|
|
85
|
+
|
|
86
|
+
User-agent: AhrefsBot
|
|
87
|
+
Crawl-delay: 10
|
|
88
|
+
${generalDisallowRules({sitemapUrl, shopId})}
|
|
89
|
+
|
|
90
|
+
User-agent: AhrefsSiteAudit
|
|
91
|
+
Crawl-delay: 10
|
|
92
|
+
${generalDisallowRules({sitemapUrl, shopId})}
|
|
93
|
+
|
|
94
|
+
User-agent: MJ12bot
|
|
95
|
+
Crawl-Delay: 10
|
|
71
96
|
|
|
72
97
|
User-agent: Pinterest
|
|
73
98
|
Crawl-delay: 1
|
|
74
99
|
`.trim();
|
|
75
100
|
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
*
|
|
104
|
+
* This function generates disallow rules that generally follow what Shopify's Online Store has as defaults for their robots.txt
|
|
105
|
+
*
|
|
106
|
+
*/
|
|
107
|
+
function generalDisallowRules({
|
|
108
|
+
shopId,
|
|
109
|
+
sitemapUrl,
|
|
110
|
+
}: {
|
|
111
|
+
shopId?: string;
|
|
112
|
+
sitemapUrl?: string;
|
|
113
|
+
}) {
|
|
114
|
+
return `Disallow: /admin
|
|
115
|
+
Disallow: /cart
|
|
116
|
+
Disallow: /orders
|
|
117
|
+
Disallow: /checkouts/
|
|
118
|
+
Disallow: /checkout${
|
|
119
|
+
shopId
|
|
120
|
+
? `
|
|
121
|
+
Disallow: /${shopId}/checkouts
|
|
122
|
+
Disallow: /${shopId}/orders`
|
|
123
|
+
: ''
|
|
124
|
+
}
|
|
125
|
+
Disallow: /carts
|
|
126
|
+
Disallow: /account
|
|
127
|
+
Disallow: /collections/*sort_by*
|
|
128
|
+
Disallow: /*/collections/*sort_by*
|
|
129
|
+
Disallow: /collections/*+*
|
|
130
|
+
Disallow: /collections/*%2B*
|
|
131
|
+
Disallow: /collections/*%2b*
|
|
132
|
+
Disallow: /*/collections/*+*
|
|
133
|
+
Disallow: /*/collections/*%2B*
|
|
134
|
+
Disallow: /*/collections/*%2b*
|
|
135
|
+
Disallow: */collections/*filter*&*filter*
|
|
136
|
+
Disallow: /blogs/*+*
|
|
137
|
+
Disallow: /blogs/*%2B*
|
|
138
|
+
Disallow: /blogs/*%2b*
|
|
139
|
+
Disallow: /*/blogs/*+*
|
|
140
|
+
Disallow: /*/blogs/*%2B*
|
|
141
|
+
Disallow: /*/blogs/*%2b*
|
|
142
|
+
Disallow: /*?*oseid=*
|
|
143
|
+
Disallow: /*preview_theme_id*
|
|
144
|
+
Disallow: /*preview_script_id*
|
|
145
|
+
Disallow: /policies/
|
|
146
|
+
Disallow: /*/*?*ls=*&ls=*
|
|
147
|
+
Disallow: /*/*?*ls%3D*%3Fls%3D*
|
|
148
|
+
Disallow: /*/*?*ls%3d*%3fls%3d*
|
|
149
|
+
Disallow: /search
|
|
150
|
+
Allow: /search/
|
|
151
|
+
Disallow: /search/?*
|
|
152
|
+
Disallow: /apple-app-site-association
|
|
153
|
+
Disallow: /.well-known/shopify/monorail${
|
|
154
|
+
sitemapUrl
|
|
155
|
+
? `
|
|
156
|
+
Sitemap: ${sitemapUrl}`
|
|
157
|
+
: ''
|
|
158
|
+
}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const SHOP_QUERY = `#graphql
|
|
162
|
+
query robots_shop_query {
|
|
163
|
+
shop {
|
|
164
|
+
id
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
`;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useLoaderData,
|
|
3
|
+
Link,
|
|
4
|
+
useRouteError,
|
|
5
|
+
isRouteErrorResponse,
|
|
6
|
+
useCatch,
|
|
7
|
+
} from '@remix-run/react';
|
|
8
|
+
import {
|
|
9
|
+
json,
|
|
10
|
+
type LoaderArgs,
|
|
11
|
+
type ErrorBoundaryComponent,
|
|
12
|
+
} from '@shopify/remix-oxygen';
|
|
13
|
+
import type {CollectionConnection} from '@shopify/hydrogen/storefront-api-types';
|
|
14
|
+
import {Image} from '@shopify/hydrogen';
|
|
15
|
+
|
|
16
|
+
export async function loader({context}: LoaderArgs) {
|
|
17
|
+
const {collectionConnection} = await context.storefront.query<{
|
|
18
|
+
collectionConnection: CollectionConnection;
|
|
19
|
+
}>(COLLECTIONS_QUERY, {
|
|
20
|
+
variables: {
|
|
21
|
+
country: context.storefront.i18n?.country,
|
|
22
|
+
language: context.storefront.i18n?.language,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return json({collectionConnection});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function Collections() {
|
|
30
|
+
const {collectionConnection} = useLoaderData<typeof loader>();
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<>
|
|
34
|
+
{collectionConnection.nodes.map((collection, collectionIndex) => {
|
|
35
|
+
return (
|
|
36
|
+
<Link key={collection.id} to={`/collections/${collection.handle}`}>
|
|
37
|
+
<h3>{collection.title}</h3>
|
|
38
|
+
{collection.image && (
|
|
39
|
+
<Image
|
|
40
|
+
data={collection.image}
|
|
41
|
+
loading={collectionIndex === 0 ? 'eager' : undefined}
|
|
42
|
+
/>
|
|
43
|
+
)}
|
|
44
|
+
</Link>
|
|
45
|
+
);
|
|
46
|
+
})}
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const ErrorBoundaryV1: ErrorBoundaryComponent = ({error}) => {
|
|
52
|
+
console.error(error);
|
|
53
|
+
|
|
54
|
+
return <div>There was an error.</div>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export function CatchBoundary() {
|
|
58
|
+
const caught = useCatch();
|
|
59
|
+
console.error(caught);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div>
|
|
63
|
+
There was an error. Status: {caught.status}. Message:{' '}
|
|
64
|
+
{caught.data?.message}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function ErrorBoundary() {
|
|
70
|
+
const error = useRouteError();
|
|
71
|
+
|
|
72
|
+
if (isRouteErrorResponse(error)) {
|
|
73
|
+
console.error(error.status, error.statusText, error.data);
|
|
74
|
+
return <div>Route Error</div>;
|
|
75
|
+
} else {
|
|
76
|
+
console.error((error as Error).message);
|
|
77
|
+
return <div>Thrown Error</div>;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const COLLECTIONS_QUERY = `#graphql
|
|
82
|
+
query collection_index(
|
|
83
|
+
$country: CountryCode
|
|
84
|
+
$language: LanguageCode
|
|
85
|
+
) @inContext(country: $country, language: $language)
|
|
86
|
+
{
|
|
87
|
+
collectionConnection: collections(first: 250) {
|
|
88
|
+
nodes {
|
|
89
|
+
id
|
|
90
|
+
title
|
|
91
|
+
handle
|
|
92
|
+
image {
|
|
93
|
+
id
|
|
94
|
+
url
|
|
95
|
+
altText
|
|
96
|
+
width
|
|
97
|
+
height
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
`;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AbortError } from '@shopify/cli-kit/node/error';
|
|
2
|
+
import { ensureAuthenticatedAdmin } from '@shopify/cli-kit/node/session';
|
|
3
|
+
|
|
4
|
+
async function getAdminSession(shop) {
|
|
5
|
+
let adminSession;
|
|
6
|
+
try {
|
|
7
|
+
adminSession = await ensureAuthenticatedAdmin(shop);
|
|
8
|
+
} catch {
|
|
9
|
+
throw new AbortError("Unable to authenticate with Shopify", void 0, [
|
|
10
|
+
`Ensure the shop that you specified is correct (you are trying to use: ${shop})`
|
|
11
|
+
]);
|
|
12
|
+
}
|
|
13
|
+
return adminSession;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { getAdminSession };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, vi, afterEach, it, expect } from 'vitest';
|
|
2
|
+
import { ensureAuthenticatedAdmin } from '@shopify/cli-kit/node/session';
|
|
3
|
+
import { AbortError } from '@shopify/cli-kit/node/error';
|
|
4
|
+
import { getAdminSession } from './admin-session.js';
|
|
5
|
+
|
|
6
|
+
describe("list", () => {
|
|
7
|
+
vi.mock("@shopify/cli-kit/node/session");
|
|
8
|
+
const ADMIN_SESSION = {
|
|
9
|
+
token: "abc123",
|
|
10
|
+
storeFqdn: "my-shop"
|
|
11
|
+
};
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.resetAllMocks();
|
|
14
|
+
});
|
|
15
|
+
it("returns the admin session", async () => {
|
|
16
|
+
vi.mocked(ensureAuthenticatedAdmin).mockResolvedValue(ADMIN_SESSION);
|
|
17
|
+
const adminSession = await getAdminSession("my-shop");
|
|
18
|
+
expect(ensureAuthenticatedAdmin).toHaveBeenCalledWith("my-shop");
|
|
19
|
+
expect(adminSession).toStrictEqual(ADMIN_SESSION);
|
|
20
|
+
});
|
|
21
|
+
describe("when it fails to authenticate", () => {
|
|
22
|
+
it("throws an error", async () => {
|
|
23
|
+
vi.mocked(ensureAuthenticatedAdmin).mockRejectedValue({});
|
|
24
|
+
await expect(getAdminSession("my-shop")).rejects.toThrow(AbortError);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AdminSession } from '@shopify/cli-kit/node/session';
|
|
2
|
+
|
|
3
|
+
declare function newHydrogenStorefrontUrl(session: AdminSession): string;
|
|
4
|
+
declare function hydrogenStorefrontsUrl(session: AdminSession): string;
|
|
5
|
+
declare function hydrogenStorefrontUrl(session: AdminSession, storefrontId: string): string;
|
|
6
|
+
declare function hydrogenStorefrontSettingsUrl(session: AdminSession, storefrontId: string): string;
|
|
7
|
+
|
|
8
|
+
export { hydrogenStorefrontSettingsUrl, hydrogenStorefrontUrl, hydrogenStorefrontsUrl, newHydrogenStorefrontUrl };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
function newHydrogenStorefrontUrl(session) {
|
|
2
|
+
const { storeFqdn } = session;
|
|
3
|
+
return `https://${storeFqdn}/admin/custom_storefronts/new`;
|
|
4
|
+
}
|
|
5
|
+
function hydrogenStorefrontsUrl(session) {
|
|
6
|
+
const { storeFqdn } = session;
|
|
7
|
+
return `https://${storeFqdn}/admin/custom_storefronts`;
|
|
8
|
+
}
|
|
9
|
+
function hydrogenStorefrontUrl(session, storefrontId) {
|
|
10
|
+
const { storeFqdn } = session;
|
|
11
|
+
return `https://${storeFqdn}/admin/custom_storefronts/${storefrontId}`;
|
|
12
|
+
}
|
|
13
|
+
function hydrogenStorefrontSettingsUrl(session, storefrontId) {
|
|
14
|
+
const { storeFqdn } = session;
|
|
15
|
+
return `https://${storeFqdn}/admin/custom_storefronts/${storefrontId}/settings`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { hydrogenStorefrontSettingsUrl, hydrogenStorefrontUrl, hydrogenStorefrontsUrl, newHydrogenStorefrontUrl };
|
package/dist/lib/flags.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ 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>;
|
|
7
8
|
};
|
|
8
9
|
declare function flagsToCamelObject(obj: Record<string, any>): any;
|
|
9
10
|
/**
|
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,12 @@ 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)
|
|
20
27
|
})
|
|
21
28
|
};
|
|
22
29
|
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 };
|