Skip to content

Commit 3bc6ff5

Browse files
committed
Add remix-hydrogen test app.
1 parent fd3df68 commit 3bc6ff5

27 files changed

+1029
-0
lines changed

.github/workflows/build.yml

+1
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,7 @@ jobs:
873873
'node-experimental-fastify-app',
874874
'remix-cloudflare-workers',
875875
'remix-cloudflare-pages',
876+
'remix-hydrogen',
876877
]
877878
build-command:
878879
- false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
build
2+
node_modules
3+
bin
4+
*.d.ts
5+
dist
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* @type {import("@types/eslint").Linter.BaseConfig}
3+
*/
4+
module.exports = {
5+
extends: ['plugin:hydrogen/recommended', 'plugin:hydrogen/typescript'],
6+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
node_modules
2+
/.cache
3+
/build
4+
/dist
5+
/public/build
6+
/.mf
7+
.env
8+
.shopify
9+
env.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
schema: node_modules/@shopify/hydrogen-react/storefront.schema.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@shopify:registry=https://registry.npmjs.com
2+
progress=false
3+
@sentry:registry=http://localhost:4873
4+
@sentry-internal:registry=http://localhost:4873
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Hydrogen template: Hello World
2+
3+
Hydrogen is Shopify’s stack for headless commerce. Hydrogen is designed to dovetail with [Remix](https://remix.run/), Shopify’s full stack web framework. This template contains a **minimal setup** of components, queries and tooling to get started with Hydrogen.
4+
5+
[Check out Hydrogen docs](https://shopify.dev/custom-storefronts/hydrogen)
6+
[Get familiar with Remix](https://remix.run/docs/en/v1)
7+
8+
## What's included
9+
10+
- Remix
11+
- Hydrogen
12+
- Oxygen
13+
- Shopify CLI
14+
- ESLint
15+
- Prettier
16+
- GraphQL generator
17+
- TypeScript and JavaScript flavors
18+
- Minimal setup of components and routes
19+
20+
## Getting started
21+
22+
**Requirements:**
23+
24+
- Node.js version 16.14.0 or higher
25+
26+
```bash
27+
npm create @shopify/hydrogen@latest -- --template hello-world
28+
```
29+
30+
Remember to update `.env` with your shop's domain and Storefront API token!
31+
32+
## Building for production
33+
34+
```bash
35+
npm run build
36+
```
37+
38+
## Local development
39+
40+
```bash
41+
npm run dev
42+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {RemixBrowser, useLocation, useMatches} from '@remix-run/react';
2+
import {startTransition, StrictMode, useEffect} from 'react';
3+
import {hydrateRoot} from 'react-dom/client';
4+
import * as Sentry from '@sentry/remix';
5+
import {env} from '../env';
6+
7+
Sentry.init({
8+
dsn: env.SENTRY_DSN,
9+
integrations: [
10+
new Sentry.BrowserTracing({
11+
routingInstrumentation: Sentry.remixRouterInstrumentation(
12+
useEffect,
13+
useLocation,
14+
useMatches,
15+
),
16+
}),
17+
new Sentry.Replay(),
18+
],
19+
// Performance Monitoring
20+
tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production!
21+
// Session Replay
22+
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
23+
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
24+
debug: true,
25+
});
26+
27+
Sentry.addGlobalEventProcessor((event) => {
28+
if (
29+
event.type === 'transaction' &&
30+
(event.contexts?.trace?.op === 'pageload' ||
31+
event.contexts?.trace?.op === 'navigation')
32+
) {
33+
const eventId = event.event_id;
34+
if (eventId) {
35+
window.recordedTransactions = window.recordedTransactions || [];
36+
window.recordedTransactions.push(eventId);
37+
}
38+
}
39+
40+
return event;
41+
});
42+
startTransition(() => {
43+
hydrateRoot(
44+
document,
45+
<StrictMode>
46+
<RemixBrowser />
47+
</StrictMode>,
48+
);
49+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type {DataFunctionArgs, EntryContext} from '@shopify/remix-oxygen';
2+
import {RemixServer} from '@remix-run/react';
3+
import isbot from 'isbot';
4+
import {renderToReadableStream} from 'react-dom/server';
5+
import {createContentSecurityPolicy} from '@shopify/hydrogen';
6+
import * as Sentry from '@sentry/remix';
7+
import {env} from '../env';
8+
9+
Sentry.workerInit({
10+
dsn: env.SENTRY_DSN,
11+
// Performance Monitoring
12+
tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production!
13+
});
14+
15+
export function handleError(error: unknown, {request}: DataFunctionArgs): void {
16+
Sentry.captureRemixServerException(error, 'remix.server', request);
17+
}
18+
19+
export default async function handleRequest(
20+
request: Request,
21+
responseStatusCode: number,
22+
responseHeaders: Headers,
23+
remixContext: EntryContext,
24+
) {
25+
const {nonce, header, NonceProvider} = createContentSecurityPolicy();
26+
27+
const body = await renderToReadableStream(
28+
<NonceProvider>
29+
<RemixServer context={remixContext} url={request.url} />
30+
</NonceProvider>,
31+
{
32+
nonce,
33+
signal: request.signal,
34+
onError(error) {
35+
// eslint-disable-next-line no-console
36+
console.error(error);
37+
responseStatusCode = 500;
38+
},
39+
},
40+
);
41+
42+
if (isbot(request.headers.get('user-agent'))) {
43+
await body.allReady;
44+
}
45+
46+
responseHeaders.set('Content-Type', 'text/html');
47+
responseHeaders.set('Content-Security-Policy', header);
48+
return new Response(body, {
49+
headers: responseHeaders,
50+
status: responseStatusCode,
51+
});
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import {type LinksFunction, type LoaderArgs} from '@shopify/remix-oxygen';
2+
import {
3+
Links,
4+
Meta,
5+
Outlet,
6+
Scripts,
7+
LiveReload,
8+
ScrollRestoration,
9+
useLoaderData,
10+
type ShouldRevalidateFunction,
11+
useRouteError,
12+
} from '@remix-run/react';
13+
import type {Shop} from '@shopify/hydrogen/storefront-api-types';
14+
import appStyles from './styles/app.css';
15+
import favicon from '../public/favicon.svg';
16+
import {useNonce} from '@shopify/hydrogen';
17+
import * as Sentry from '@sentry/remix';
18+
19+
// This is important to avoid re-fetching root queries on sub-navigations
20+
export const shouldRevalidate: ShouldRevalidateFunction = ({
21+
formMethod,
22+
currentUrl,
23+
nextUrl,
24+
}) => {
25+
// revalidate when a mutation is performed e.g add to cart, login...
26+
if (formMethod && formMethod !== 'GET') {
27+
return true;
28+
}
29+
30+
// revalidate when manually revalidating via useRevalidator
31+
if (currentUrl.toString() === nextUrl.toString()) {
32+
return true;
33+
}
34+
35+
return false;
36+
};
37+
38+
export const links: LinksFunction = () => {
39+
return [
40+
{rel: 'stylesheet', href: appStyles},
41+
{
42+
rel: 'preconnect',
43+
href: 'https://cdn.shopify.com',
44+
},
45+
{
46+
rel: 'preconnect',
47+
href: 'https://shop.app',
48+
},
49+
{rel: 'icon', type: 'image/svg+xml', href: favicon},
50+
];
51+
};
52+
53+
export async function loader({context}: LoaderArgs) {
54+
const layout = await context.storefront.query<{shop: Shop}>(LAYOUT_QUERY);
55+
return {
56+
layout,
57+
};
58+
}
59+
60+
export function ErrorBoundary() {
61+
const error = useRouteError();
62+
const eventId = Sentry.captureRemixErrorBoundaryError(error);
63+
64+
return (
65+
<div>
66+
<span>ErrorBoundary Error</span>
67+
<span id="event-id">{eventId}</span>
68+
</div>
69+
);
70+
}
71+
72+
function App() {
73+
const nonce = useNonce();
74+
const data = useLoaderData<typeof loader>();
75+
76+
const {name} = data.layout.shop;
77+
78+
return (
79+
<html lang="en">
80+
<head>
81+
<meta charSet="utf-8" />
82+
<meta name="viewport" content="width=device-width,initial-scale=1" />
83+
<Meta />
84+
<Links />
85+
</head>
86+
<body>
87+
<h1>Hello, {name}</h1>
88+
<p>This is a custom storefront powered by Hydrogen</p>
89+
<Outlet />
90+
<ScrollRestoration nonce={nonce} />
91+
<Scripts nonce={nonce} />
92+
<LiveReload nonce={nonce} />
93+
</body>
94+
</html>
95+
);
96+
}
97+
98+
export default Sentry.withSentry(App);
99+
100+
const LAYOUT_QUERY = `#graphql
101+
query layout {
102+
shop {
103+
name
104+
description
105+
}
106+
}
107+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import * as Sentry from '@sentry/remix';
2+
import {Link} from '@remix-run/react';
3+
4+
export default function Index() {
5+
return (
6+
<div>
7+
<input
8+
type="button"
9+
value="Capture Exception"
10+
id="exception-button"
11+
onClick={() => {
12+
const eventId = Sentry.captureException(new Error('I am an error!'));
13+
window.capturedExceptionId = eventId;
14+
}}
15+
/>
16+
<Link to="/user/5" id="navigation">
17+
navigate
18+
</Link>
19+
</div>
20+
);
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import {useEffect, useState} from 'react';
2+
3+
export default function ErrorBoundaryCapture() {
4+
const [count, setCount] = useState(0);
5+
6+
useEffect(() => {
7+
if (count > 0) {
8+
throw new Error('Sentry React Component Error');
9+
} else {
10+
setTimeout(() => setCount(count + 1), 10);
11+
}
12+
}, [count]);
13+
14+
return <div>{count}</div>;
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { LoaderFunction } from '@remix-run/node';
2+
import { useLoaderData } from '@remix-run/react';
3+
4+
export const loader: LoaderFunction = async ({ params: { id } }) => {
5+
if (id === '-1') {
6+
throw new Error('Unexpected Server Error');
7+
}
8+
9+
return null;
10+
};
11+
12+
export default function LoaderError() {
13+
const data = useLoaderData();
14+
15+
return (
16+
<div>
17+
<h1>{data && data.test ? data.test : 'Not Found'}</h1>
18+
</div>
19+
);
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {json} from '@shopify/remix-oxygen';
2+
import {useLoaderData} from '@remix-run/react';
3+
import * as Sentry from '@sentry/remix';
4+
5+
export function loader() {
6+
const id = Sentry.captureException(new Error('Sentry Server Error'));
7+
8+
return json({id});
9+
}
10+
11+
export default function ServerError() {
12+
const {id} = useLoaderData();
13+
14+
return (
15+
<div>
16+
<pre id="event-id">{id}</pre>
17+
</div>
18+
);
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function User() {
2+
return <div>I am a blank page</div>;
3+
}

0 commit comments

Comments
 (0)