@shopify/cli-hydrogen 4.0.8 → 4.1.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 (76) hide show
  1. package/README.md +9 -0
  2. package/dist/commands/hydrogen/build.d.ts +4 -1
  3. package/dist/commands/hydrogen/build.js +21 -17
  4. package/dist/commands/hydrogen/check.d.ts +3 -6
  5. package/dist/commands/hydrogen/check.js +10 -9
  6. package/dist/commands/hydrogen/dev.d.ts +3 -2
  7. package/dist/commands/hydrogen/dev.js +24 -22
  8. package/dist/commands/hydrogen/g.d.ts +10 -0
  9. package/dist/commands/hydrogen/g.js +17 -0
  10. package/dist/commands/hydrogen/generate/route.d.ts +7 -9
  11. package/dist/commands/hydrogen/generate/route.js +49 -47
  12. package/dist/commands/hydrogen/generate/route.test.js +48 -40
  13. package/dist/commands/hydrogen/generate/routes.d.ts +2 -2
  14. package/dist/commands/hydrogen/init.d.ts +3 -3
  15. package/dist/commands/hydrogen/init.js +76 -95
  16. package/dist/commands/hydrogen/init.test.js +126 -0
  17. package/dist/commands/hydrogen/preview.d.ts +2 -2
  18. package/dist/commands/hydrogen/preview.js +4 -4
  19. package/dist/commands/hydrogen/shortcut.d.ts +9 -0
  20. package/dist/commands/hydrogen/shortcut.js +74 -0
  21. package/dist/commands/hydrogen/shortcut.test.js +58 -0
  22. package/dist/generator-templates/routes/[robots.txt].tsx +35 -1
  23. package/dist/generator-templates/routes/[sitemap.xml].tsx +45 -10
  24. package/dist/generator-templates/routes/account/login.tsx +42 -13
  25. package/dist/generator-templates/routes/account/register.tsx +42 -13
  26. package/dist/generator-templates/routes/cart.tsx +42 -2
  27. package/dist/generator-templates/routes/collections/$collectionHandle.tsx +44 -5
  28. package/dist/generator-templates/routes/index.tsx +33 -0
  29. package/dist/generator-templates/routes/pages/$pageHandle.tsx +48 -10
  30. package/dist/generator-templates/routes/policies/$policyHandle.tsx +67 -14
  31. package/dist/generator-templates/routes/policies/index.tsx +54 -4
  32. package/dist/generator-templates/routes/products/$productHandle.tsx +44 -9
  33. package/dist/hooks/init.js +2 -2
  34. package/dist/{utils → lib}/check-lockfile.js +7 -4
  35. package/dist/{utils → lib}/check-lockfile.test.js +19 -28
  36. package/dist/{utils → lib}/check-version.test.js +3 -2
  37. package/dist/lib/colors.d.ts +8 -0
  38. package/dist/lib/colors.js +8 -0
  39. package/dist/{utils → lib}/config.js +10 -19
  40. package/dist/{utils → lib}/flags.d.ts +9 -3
  41. package/dist/{utils → lib}/flags.js +19 -4
  42. package/dist/lib/flags.test.d.ts +1 -0
  43. package/dist/{utils → lib}/mini-oxygen.js +14 -12
  44. package/dist/{utils → lib}/missing-routes.js +1 -1
  45. package/dist/lib/remix-version-interop.d.ts +11 -0
  46. package/dist/lib/remix-version-interop.js +54 -0
  47. package/dist/lib/remix-version-interop.test.d.ts +1 -0
  48. package/dist/lib/remix-version-interop.test.js +93 -0
  49. package/dist/lib/shell.d.ts +12 -0
  50. package/dist/lib/shell.js +73 -0
  51. package/dist/lib/template-downloader.d.ts +6 -0
  52. package/dist/{utils → lib}/template-downloader.js +21 -16
  53. package/dist/{utils → lib}/transpile-ts.js +5 -5
  54. package/dist/lib/virtual-routes.test.d.ts +1 -0
  55. package/dist/virtual-routes/routes/index.jsx +2 -15
  56. package/dist/virtual-routes/virtual-root.jsx +5 -6
  57. package/oclif.manifest.json +1 -1
  58. package/package.json +11 -10
  59. package/dist/utils/template-downloader.d.ts +0 -11
  60. /package/dist/{utils/check-lockfile.test.d.ts → commands/hydrogen/init.test.d.ts} +0 -0
  61. /package/dist/{utils/check-version.test.d.ts → commands/hydrogen/shortcut.test.d.ts} +0 -0
  62. /package/dist/{utils → lib}/check-lockfile.d.ts +0 -0
  63. /package/dist/{utils/flags.test.d.ts → lib/check-lockfile.test.d.ts} +0 -0
  64. /package/dist/{utils → lib}/check-version.d.ts +0 -0
  65. /package/dist/{utils → lib}/check-version.js +0 -0
  66. /package/dist/{utils/virtual-routes.test.d.ts → lib/check-version.test.d.ts} +0 -0
  67. /package/dist/{utils → lib}/config.d.ts +0 -0
  68. /package/dist/{utils → lib}/flags.test.js +0 -0
  69. /package/dist/{utils → lib}/log.d.ts +0 -0
  70. /package/dist/{utils → lib}/log.js +0 -0
  71. /package/dist/{utils → lib}/mini-oxygen.d.ts +0 -0
  72. /package/dist/{utils → lib}/missing-routes.d.ts +0 -0
  73. /package/dist/{utils → lib}/transpile-ts.d.ts +0 -0
  74. /package/dist/{utils → lib}/virtual-routes.d.ts +0 -0
  75. /package/dist/{utils → lib}/virtual-routes.js +0 -0
  76. /package/dist/{utils → lib}/virtual-routes.test.js +0 -0
@@ -0,0 +1,58 @@
1
+ import { describe, beforeEach, vi, afterEach, expect, it } from 'vitest';
2
+ import { runCreateShortcut } from './shortcut.js';
3
+ import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output';
4
+ import { supportsShell, isWindows, isGitBash } from '../../lib/shell.js';
5
+ import { execSync, exec } from 'child_process';
6
+
7
+ describe("shortcut", () => {
8
+ const outputMock = mockAndCaptureOutput();
9
+ beforeEach(() => {
10
+ vi.resetAllMocks();
11
+ vi.mock("child_process");
12
+ vi.mock("../../lib/shell.js", async () => {
13
+ return {
14
+ isWindows: vi.fn(),
15
+ isGitBash: vi.fn(),
16
+ supportsShell: vi.fn(),
17
+ shellWriteFile: () => true,
18
+ shellRunScript: () => true,
19
+ hasAlias: () => false,
20
+ homeFileExists: () => Promise.resolve(true)
21
+ };
22
+ });
23
+ vi.mocked(supportsShell).mockImplementation(
24
+ (shell) => !isWindows() || shell === "bash"
25
+ );
26
+ });
27
+ afterEach(() => {
28
+ outputMock.clear();
29
+ expect(execSync).toHaveBeenCalledTimes(0);
30
+ expect(exec).toHaveBeenCalledTimes(0);
31
+ });
32
+ it("creates aliases for Unix", async () => {
33
+ vi.mocked(isWindows).mockReturnValue(false);
34
+ await runCreateShortcut();
35
+ expect(outputMock.info()).toMatch(`zsh, bash, fish`);
36
+ expect(outputMock.error()).toBeFalsy();
37
+ });
38
+ it("creates aliases for Windows", async () => {
39
+ vi.mocked(isWindows).mockReturnValue(true);
40
+ await runCreateShortcut();
41
+ expect(outputMock.info()).toMatch(`PowerShell, PowerShell 7+`);
42
+ expect(outputMock.error()).toBeFalsy();
43
+ });
44
+ it("creates aliases for Windows in Git Bash", async () => {
45
+ vi.mocked(isWindows).mockReturnValue(true);
46
+ vi.mocked(isGitBash).mockReturnValueOnce(true);
47
+ await runCreateShortcut();
48
+ expect(outputMock.info()).toMatch("bash");
49
+ expect(outputMock.error()).toBeFalsy();
50
+ });
51
+ it("warns when not finding shells", async () => {
52
+ vi.mocked(isWindows).mockReturnValue(false);
53
+ vi.mocked(supportsShell).mockReturnValue(false);
54
+ await runCreateShortcut();
55
+ expect(outputMock.info()).toBeFalsy();
56
+ expect(outputMock.error()).toBeTruthy();
57
+ });
58
+ });
@@ -1,4 +1,8 @@
1
- import {type LoaderArgs} from '@shopify/remix-oxygen';
1
+ import {
2
+ type LoaderArgs,
3
+ type ErrorBoundaryComponent,
4
+ } from '@shopify/remix-oxygen';
5
+ import {useCatch, useRouteError, isRouteErrorResponse} from '@remix-run/react';
2
6
 
3
7
  export const loader = ({request}: LoaderArgs) => {
4
8
  const url = new URL(request.url);
@@ -14,6 +18,36 @@ export const loader = ({request}: LoaderArgs) => {
14
18
  });
15
19
  };
16
20
 
21
+ export const ErrorBoundaryV1: ErrorBoundaryComponent = ({error}) => {
22
+ console.error(error);
23
+
24
+ return <div>There was an error.</div>;
25
+ };
26
+
27
+ export function CatchBoundary() {
28
+ const caught = useCatch();
29
+ console.error(caught);
30
+
31
+ return (
32
+ <div>
33
+ There was an error. Status: {caught.status}. Message:{' '}
34
+ {caught.data?.message}
35
+ </div>
36
+ );
37
+ }
38
+
39
+ export function ErrorBoundary() {
40
+ const error = useRouteError();
41
+
42
+ if (isRouteErrorResponse(error)) {
43
+ console.error(error.status, error.statusText, error.data);
44
+ return <div>Route Error</div>;
45
+ } else {
46
+ console.error((error as Error).message);
47
+ return <div>Thrown Error</div>;
48
+ }
49
+ }
50
+
17
51
  function robotsTxtData({url}: {url: string}) {
18
52
  const sitemapUrl = url ? `${url}/sitemap.xml` : undefined;
19
53
 
@@ -1,5 +1,6 @@
1
1
  import {flattenConnection} from '@shopify/hydrogen';
2
- import type {LoaderArgs} from '@shopify/remix-oxygen';
2
+ import type {LoaderArgs, ErrorBoundaryComponent} from '@shopify/remix-oxygen';
3
+ import {useCatch, useRouteError, isRouteErrorResponse} from '@remix-run/react';
3
4
  import {
4
5
  CollectionConnection,
5
6
  PageConnection,
@@ -29,12 +30,12 @@ export async function loader({request, context: {storefront}}: LoaderArgs) {
29
30
  const data = await storefront.query<SitemapQueryData>(SITEMAP_QUERY, {
30
31
  variables: {
31
32
  urlLimits: MAX_URLS,
32
- language: storefront.i18n?.language,
33
+ language: storefront.i18n.language,
33
34
  },
34
35
  });
35
36
 
36
37
  if (!data) {
37
- throw new Response(null, {status: 404});
38
+ throw new Response('No data found', {status: 404});
38
39
  }
39
40
 
40
41
  return new Response(
@@ -50,6 +51,40 @@ export async function loader({request, context: {storefront}}: LoaderArgs) {
50
51
  );
51
52
  }
52
53
 
54
+ export const ErrorBoundaryV1: ErrorBoundaryComponent = ({error}) => {
55
+ console.error(error);
56
+
57
+ return <div>There was an error.</div>;
58
+ };
59
+
60
+ export function CatchBoundary() {
61
+ const caught = useCatch();
62
+ console.error(caught);
63
+
64
+ return (
65
+ <div>
66
+ There was an error. Status: {caught.status}. Message:{' '}
67
+ {caught.data?.message}
68
+ </div>
69
+ );
70
+ }
71
+
72
+ export function ErrorBoundary() {
73
+ const error = useRouteError();
74
+
75
+ if (isRouteErrorResponse(error)) {
76
+ console.error(error.status, error.statusText, error.data);
77
+ return <div>Route Error</div>;
78
+ } else {
79
+ console.error((error as Error).message);
80
+ return <div>Thrown Error</div>;
81
+ }
82
+ }
83
+
84
+ function xmlEncode(string: string) {
85
+ return string.replace(/[&<>'"]/g, (char) => `&#${char.charCodeAt(0)};`);
86
+ }
87
+
53
88
  function shopSitemap({
54
89
  data,
55
90
  baseUrl,
@@ -60,25 +95,25 @@ function shopSitemap({
60
95
  const productsData = flattenConnection(data.products)
61
96
  .filter((product) => product.onlineStoreUrl)
62
97
  .map((product) => {
63
- const url = `${baseUrl}/products/${product.handle}`;
98
+ const url = `${baseUrl}/products/${xmlEncode(product.handle)}`;
64
99
 
65
100
  const finalObject: ProductEntry = {
66
101
  url,
67
- lastMod: product.updatedAt!,
102
+ lastMod: product.updatedAt,
68
103
  changeFreq: 'daily',
69
104
  };
70
105
 
71
106
  if (product.featuredImage?.url) {
72
107
  finalObject.image = {
73
- url: product.featuredImage!.url,
108
+ url: xmlEncode(product.featuredImage.url),
74
109
  };
75
110
 
76
111
  if (product.title) {
77
- finalObject.image.title = product.title;
112
+ finalObject.image.title = xmlEncode(product.title);
78
113
  }
79
114
 
80
- if (product.featuredImage!.altText) {
81
- finalObject.image.caption = product.featuredImage!.altText;
115
+ if (product.featuredImage.altText) {
116
+ finalObject.image.caption = xmlEncode(product.featuredImage.altText);
82
117
  }
83
118
  }
84
119
 
@@ -116,7 +151,7 @@ function shopSitemap({
116
151
  xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
117
152
  xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
118
153
  >
119
- ${urlsDatas.map((url) => renderUrlTag(url!)).join('')}
154
+ ${urlsDatas.map((url) => renderUrlTag(url)).join('')}
120
155
  </urlset>`;
121
156
  }
122
157
 
@@ -1,12 +1,15 @@
1
1
  import {
2
- type MetaFunction,
3
2
  type ActionFunction,
4
3
  type LoaderArgs,
4
+ type ErrorBoundaryComponent,
5
5
  redirect,
6
- json,
7
6
  } from '@shopify/remix-oxygen';
8
- import {Form, Link, useActionData, useLoaderData} from '@remix-run/react';
9
- import {useState} from 'react';
7
+ import {
8
+ Form,
9
+ useCatch,
10
+ useRouteError,
11
+ isRouteErrorResponse,
12
+ } from '@remix-run/react';
10
13
 
11
14
  export async function loader({context, params}: LoaderArgs) {
12
15
  const customerAccessToken = await context.session.get('customerAccessToken');
@@ -14,15 +17,11 @@ export async function loader({context, params}: LoaderArgs) {
14
17
  if (customerAccessToken) {
15
18
  return redirect(params.lang ? `${params.lang}/account` : '/account');
16
19
  }
17
- }
18
20
 
19
- type ActionData = {
20
- formError?: string;
21
- };
22
-
23
- const badRequest = (data: ActionData) => json(data, {status: 400});
21
+ return new Response(null);
22
+ }
24
23
 
25
- export const action: ActionFunction = async ({request, context, params}) => {
24
+ export const action: ActionFunction = async ({request}) => {
26
25
  const formData = await request.formData();
27
26
 
28
27
  const email = formData.get('email');
@@ -34,8 +33,8 @@ export const action: ActionFunction = async ({request, context, params}) => {
34
33
  typeof email !== 'string' ||
35
34
  typeof password !== 'string'
36
35
  ) {
37
- return badRequest({
38
- formError: 'Please provide both an email and a password.',
36
+ throw new Response('Please provide both an email and a password.', {
37
+ status: 400,
39
38
  });
40
39
  }
41
40
 
@@ -72,3 +71,33 @@ export default function Login() {
72
71
  </Form>
73
72
  );
74
73
  }
74
+
75
+ export const ErrorBoundaryV1: ErrorBoundaryComponent = ({error}) => {
76
+ console.error(error);
77
+
78
+ return <div>There was an error.</div>;
79
+ };
80
+
81
+ export function CatchBoundary() {
82
+ const caught = useCatch();
83
+ console.error(caught);
84
+
85
+ return (
86
+ <div>
87
+ There was an error. Status: {caught.status}. Message:{' '}
88
+ {caught.data?.message}
89
+ </div>
90
+ );
91
+ }
92
+
93
+ export function ErrorBoundary() {
94
+ const error = useRouteError();
95
+
96
+ if (isRouteErrorResponse(error)) {
97
+ console.error(error.status, error.statusText, error.data);
98
+ return <div>Route Error</div>;
99
+ } else {
100
+ console.error((error as Error).message);
101
+ return <div>Thrown Error</div>;
102
+ }
103
+ }
@@ -1,12 +1,15 @@
1
1
  import {
2
- type MetaFunction,
3
2
  type ActionFunction,
4
3
  type LoaderArgs,
4
+ type ErrorBoundaryComponent,
5
5
  redirect,
6
- json,
7
6
  } from '@shopify/remix-oxygen';
8
- import {Form, Link, useActionData, useLoaderData} from '@remix-run/react';
9
- import {useState} from 'react';
7
+ import {
8
+ Form,
9
+ useCatch,
10
+ useRouteError,
11
+ isRouteErrorResponse,
12
+ } from '@remix-run/react';
10
13
 
11
14
  export async function loader({context, params}: LoaderArgs) {
12
15
  const customerAccessToken = await context.session.get('customerAccessToken');
@@ -14,15 +17,11 @@ export async function loader({context, params}: LoaderArgs) {
14
17
  if (customerAccessToken) {
15
18
  return redirect(params.lang ? `${params.lang}/account` : '/account');
16
19
  }
17
- }
18
20
 
19
- type ActionData = {
20
- formError?: string;
21
- };
22
-
23
- const badRequest = (data: ActionData) => json(data, {status: 400});
21
+ return new Response(null);
22
+ }
24
23
 
25
- export const action: ActionFunction = async ({request, context, params}) => {
24
+ export const action: ActionFunction = async ({request}) => {
26
25
  const formData = await request.formData();
27
26
 
28
27
  const email = formData.get('email');
@@ -34,8 +33,8 @@ export const action: ActionFunction = async ({request, context, params}) => {
34
33
  typeof email !== 'string' ||
35
34
  typeof password !== 'string'
36
35
  ) {
37
- return badRequest({
38
- formError: 'Please provide both an email and a password.',
36
+ throw new Response('Please provide both an email and a password.', {
37
+ status: 404,
39
38
  });
40
39
  }
41
40
 
@@ -72,3 +71,33 @@ export default function Register() {
72
71
  </Form>
73
72
  );
74
73
  }
74
+
75
+ export const ErrorBoundaryV1: ErrorBoundaryComponent = ({error}) => {
76
+ console.error(error);
77
+
78
+ return <div>There was an error.</div>;
79
+ };
80
+
81
+ export function CatchBoundary() {
82
+ const caught = useCatch();
83
+ console.error(caught);
84
+
85
+ return (
86
+ <div>
87
+ There was an error. Status: {caught.status}. Message:{' '}
88
+ {caught.data?.message}
89
+ </div>
90
+ );
91
+ }
92
+
93
+ export function ErrorBoundary() {
94
+ const error = useRouteError();
95
+
96
+ if (isRouteErrorResponse(error)) {
97
+ console.error(error.status, error.statusText, error.data);
98
+ return <div>Route Error</div>;
99
+ } else {
100
+ console.error((error as Error).message);
101
+ return <div>Thrown Error</div>;
102
+ }
103
+ }
@@ -1,7 +1,14 @@
1
- import {Await, useMatches} from '@remix-run/react';
1
+ import {
2
+ Await,
3
+ useMatches,
4
+ useCatch,
5
+ useRouteError,
6
+ isRouteErrorResponse,
7
+ } from '@remix-run/react';
2
8
  import {Suspense} from 'react';
3
9
  import {flattenConnection} from '@shopify/hydrogen';
4
10
  import type {Cart as CartType} from '@shopify/hydrogen/storefront-api-types';
11
+ import {type ErrorBoundaryComponent} from '@shopify/remix-oxygen';
5
12
 
6
13
  export async function action() {
7
14
  // @TODO implement cart action
@@ -11,7 +18,10 @@ export default function CartRoute() {
11
18
  const [root] = useMatches();
12
19
  return (
13
20
  <Suspense fallback="loading">
14
- <Await resolve={root.data?.cart as CartType}>
21
+ <Await
22
+ resolve={root.data?.cart as CartType}
23
+ errorElement={<div>An error occurred</div>}
24
+ >
15
25
  {(cart) => {
16
26
  const linesCount = Boolean(cart?.lines?.edges?.length || 0);
17
27
  if (!linesCount) {
@@ -39,3 +49,33 @@ export default function CartRoute() {
39
49
  </Suspense>
40
50
  );
41
51
  }
52
+
53
+ export const ErrorBoundaryV1: ErrorBoundaryComponent = ({error}) => {
54
+ console.error(error);
55
+
56
+ return <div>There was an error.</div>;
57
+ };
58
+
59
+ export function CatchBoundary() {
60
+ const caught = useCatch();
61
+ console.error(caught);
62
+
63
+ return (
64
+ <div>
65
+ There was an error. Status: {caught.status}. Message:{' '}
66
+ {caught.data?.message}
67
+ </div>
68
+ );
69
+ }
70
+
71
+ export function ErrorBoundary() {
72
+ const error = useRouteError();
73
+
74
+ if (isRouteErrorResponse(error)) {
75
+ console.error(error.status, error.statusText, error.data);
76
+ return <div>Route Error</div>;
77
+ } else {
78
+ console.error((error as Error).message);
79
+ return <div>Thrown Error</div>;
80
+ }
81
+ }
@@ -1,9 +1,18 @@
1
- import {json, type LoaderArgs} from '@shopify/remix-oxygen';
2
- import {useLoaderData} from '@remix-run/react';
1
+ import {
2
+ json,
3
+ type LoaderArgs,
4
+ type ErrorBoundaryComponent,
5
+ } from '@shopify/remix-oxygen';
6
+ import {
7
+ useLoaderData,
8
+ Link,
9
+ useCatch,
10
+ useRouteError,
11
+ isRouteErrorResponse,
12
+ } from '@remix-run/react';
3
13
  import type {Collection as CollectionType} from '@shopify/hydrogen/storefront-api-types';
4
- import {Link} from '@remix-run/react';
5
14
 
6
- export async function loader({params, request, context}: LoaderArgs) {
15
+ export async function loader({params, context}: LoaderArgs) {
7
16
  const {collectionHandle} = params;
8
17
 
9
18
  const {collection} = await context.storefront.query<{
@@ -38,8 +47,38 @@ export default function Collection() {
38
47
  );
39
48
  }
40
49
 
50
+ export const ErrorBoundaryV1: ErrorBoundaryComponent = ({error}) => {
51
+ console.error(error);
52
+
53
+ return <div>There was an error.</div>;
54
+ };
55
+
56
+ export function CatchBoundary() {
57
+ const caught = useCatch();
58
+ console.error(caught);
59
+
60
+ return (
61
+ <div>
62
+ There was an error. Status: {caught.status}. Message:{' '}
63
+ {caught.data?.message}
64
+ </div>
65
+ );
66
+ }
67
+
68
+ export function ErrorBoundary() {
69
+ const error = useRouteError();
70
+
71
+ if (isRouteErrorResponse(error)) {
72
+ console.error(error.status, error.statusText, error.data);
73
+ return <div>Route Error</div>;
74
+ } else {
75
+ console.error((error as Error).message);
76
+ return <div>Thrown Error</div>;
77
+ }
78
+ }
79
+
41
80
  const COLLECTION_QUERY = `#graphql
42
- query CollectionDetails(
81
+ query collection_details(
43
82
  $handle: String!
44
83
  $country: CountryCode
45
84
  $language: LanguageCode
@@ -1,3 +1,6 @@
1
+ import {type ErrorBoundaryComponent} from '@shopify/remix-oxygen';
2
+ import {useCatch, useRouteError, isRouteErrorResponse} from '@remix-run/react';
3
+
1
4
  export default function Index() {
2
5
  return (
3
6
  <p>
@@ -5,3 +8,33 @@ export default function Index() {
5
8
  </p>
6
9
  );
7
10
  }
11
+
12
+ export const ErrorBoundaryV1: ErrorBoundaryComponent = ({error}) => {
13
+ console.error(error);
14
+
15
+ return <div>There was an error.</div>;
16
+ };
17
+
18
+ export function CatchBoundary() {
19
+ const caught = useCatch();
20
+ console.error(caught);
21
+
22
+ return (
23
+ <div>
24
+ There was an error. Status: {caught.status}. Message:{' '}
25
+ {caught.data?.message}
26
+ </div>
27
+ );
28
+ }
29
+
30
+ export function ErrorBoundary() {
31
+ const error = useRouteError();
32
+
33
+ if (isRouteErrorResponse(error)) {
34
+ console.error(error.status, error.statusText, error.data);
35
+ return <div>Route Error</div>;
36
+ } else {
37
+ console.error((error as Error).message);
38
+ return <div>Thrown Error</div>;
39
+ }
40
+ }
@@ -2,15 +2,22 @@ import {
2
2
  json,
3
3
  type MetaFunction,
4
4
  type LoaderArgs,
5
- SerializeFrom,
5
+ type ErrorBoundaryComponent,
6
6
  } from '@shopify/remix-oxygen';
7
- import {useLoaderData} from '@remix-run/react';
8
- import invariant from 'tiny-invariant';
7
+ import {
8
+ useLoaderData,
9
+ type V2_MetaFunction,
10
+ useCatch,
11
+ useRouteError,
12
+ isRouteErrorResponse,
13
+ } from '@remix-run/react';
9
14
  import type {Page as PageType} from '@shopify/hydrogen/storefront-api-types';
10
15
  import type {SeoHandleFunction} from '@shopify/hydrogen';
11
16
 
12
17
  export async function loader({params, context}: LoaderArgs) {
13
- invariant(params.pageHandle, 'Missing page handle');
18
+ if (!params.pageHandle) {
19
+ throw new Error('Missing page handle');
20
+ }
14
21
 
15
22
  const {page} = await context.storefront.query<{page: PageType}>(PAGE_QUERY, {
16
23
  variables: {
@@ -36,13 +43,14 @@ export const handle = {
36
43
  seo,
37
44
  };
38
45
 
39
- export const meta: MetaFunction = ({data}) => {
46
+ export const metaV1: MetaFunction = ({data}) => {
40
47
  const {title, description} = data?.page.seo ?? {};
48
+ return {title, description};
49
+ };
41
50
 
42
- return {
43
- title,
44
- description,
45
- };
51
+ export const meta: V2_MetaFunction = ({data}) => {
52
+ const {title, description} = data?.page.seo ?? {};
53
+ return [{title}, {name: 'description', content: description}];
46
54
  };
47
55
 
48
56
  export default function Page() {
@@ -58,8 +66,38 @@ export default function Page() {
58
66
  );
59
67
  }
60
68
 
69
+ export const ErrorBoundaryV1: ErrorBoundaryComponent = ({error}) => {
70
+ console.error(error);
71
+
72
+ return <div>There was an error.</div>;
73
+ };
74
+
75
+ export function CatchBoundary() {
76
+ const caught = useCatch();
77
+ console.error(caught);
78
+
79
+ return (
80
+ <div>
81
+ There was an error. Status: {caught.status}. Message:{' '}
82
+ {caught.data?.message}
83
+ </div>
84
+ );
85
+ }
86
+
87
+ export function ErrorBoundary() {
88
+ const error = useRouteError();
89
+
90
+ if (isRouteErrorResponse(error)) {
91
+ console.error(error.status, error.statusText, error.data);
92
+ return <div>Route Error</div>;
93
+ } else {
94
+ console.error((error as Error).message);
95
+ return <div>Thrown Error</div>;
96
+ }
97
+ }
98
+
61
99
  const PAGE_QUERY = `#graphql
62
- query PageDetails($language: LanguageCode, $handle: String!)
100
+ query page_details($language: LanguageCode, $handle: String!)
63
101
  @inContext(language: $language) {
64
102
  page(handle: $handle) {
65
103
  id