Skip to content

Commit ce66380

Browse files
authored
feat(react-router): Add support for Hydrogen with RR7 (#17145)
1 parent ac57eb1 commit ce66380

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1565
-36
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
SESSION_SECRET = "foo"
2+
PUBLIC_STORE_DOMAIN="mock.shop"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
build
2+
node_modules
3+
bin
4+
*.d.ts
5+
dist
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* This is intended to be a basic starting point for linting in your app.
3+
* It relies on recommended configs out of the box for simplicity, but you can
4+
* and should modify this configuration to best suit your team's needs.
5+
*/
6+
7+
/** @type {import('eslint').Linter.Config} */
8+
module.exports = {
9+
root: true,
10+
parserOptions: {
11+
ecmaVersion: 'latest',
12+
sourceType: 'module',
13+
ecmaFeatures: {
14+
jsx: true,
15+
},
16+
},
17+
env: {
18+
browser: true,
19+
commonjs: true,
20+
es6: true,
21+
},
22+
23+
// Base config
24+
extends: ['eslint:recommended'],
25+
26+
overrides: [
27+
// React
28+
{
29+
files: ['**/*.{js,jsx,ts,tsx}'],
30+
plugins: ['react', 'jsx-a11y'],
31+
extends: [
32+
'plugin:react/recommended',
33+
'plugin:react/jsx-runtime',
34+
'plugin:react-hooks/recommended',
35+
'plugin:jsx-a11y/recommended',
36+
],
37+
settings: {
38+
react: {
39+
version: 'detect',
40+
},
41+
formComponents: ['Form'],
42+
linkComponents: [
43+
{ name: 'Link', linkAttribute: 'to' },
44+
{ name: 'NavLink', linkAttribute: 'to' },
45+
],
46+
'import/resolver': {
47+
typescript: {},
48+
},
49+
},
50+
},
51+
52+
// Typescript
53+
{
54+
files: ['**/*.{ts,tsx}'],
55+
plugins: ['@typescript-eslint', 'import'],
56+
parser: '@typescript-eslint/parser',
57+
settings: {
58+
'import/internal-regex': '^~/',
59+
'import/resolver': {
60+
node: {
61+
extensions: ['.ts', '.tsx'],
62+
},
63+
typescript: {
64+
alwaysTryTypes: true,
65+
},
66+
},
67+
},
68+
extends: ['plugin:@typescript-eslint/recommended', 'plugin:import/recommended', 'plugin:import/typescript'],
69+
},
70+
71+
// Node
72+
{
73+
files: ['.eslintrc.cjs', 'server.ts'],
74+
env: {
75+
node: true,
76+
},
77+
},
78+
],
79+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
node_modules
2+
/.cache
3+
/build
4+
/dist
5+
/public/build
6+
/.mf
7+
!.env
8+
.shopify
9+
storefrontapi.generated.d.ts
10+
.react-router
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { HydratedRouter } from 'react-router/dom';
2+
import * as Sentry from '@sentry/react-router/cloudflare';
3+
import { StrictMode, startTransition } from 'react';
4+
import { hydrateRoot } from 'react-dom/client';
5+
6+
Sentry.init({
7+
environment: 'qa', // dynamic sampling bias to keep transactions
8+
// Could not find a working way to set the DSN in the browser side from the environment variables
9+
dsn: 'https://[email protected]/1337',
10+
integrations: [Sentry.reactRouterTracingIntegration()],
11+
tracesSampleRate: 1.0,
12+
tunnel: 'http://localhost:3031/', // proxy server
13+
});
14+
15+
startTransition(() => {
16+
hydrateRoot(
17+
document,
18+
<StrictMode>
19+
<HydratedRouter />
20+
</StrictMode>,
21+
);
22+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import '../instrument.server';
2+
import { HandleErrorFunction, ServerRouter } from 'react-router';
3+
import { createContentSecurityPolicy } from '@shopify/hydrogen';
4+
import type { EntryContext } from '@shopify/remix-oxygen';
5+
import { renderToReadableStream } from 'react-dom/server';
6+
import * as Sentry from '@sentry/react-router/cloudflare';
7+
8+
async function handleRequest(
9+
request: Request,
10+
responseStatusCode: number,
11+
responseHeaders: Headers,
12+
reactRouterContext: EntryContext,
13+
) {
14+
const { nonce, header, NonceProvider } = createContentSecurityPolicy({
15+
connectSrc: [
16+
// Need to allow the proxy server to fetch the data
17+
'http://localhost:3031/',
18+
],
19+
});
20+
21+
const body = Sentry.injectTraceMetaTags(await renderToReadableStream(
22+
<NonceProvider>
23+
<ServerRouter context={reactRouterContext} url={request.url} nonce={nonce} />
24+
</NonceProvider>,
25+
{
26+
nonce,
27+
signal: request.signal,
28+
},
29+
));
30+
31+
responseHeaders.set('Content-Type', 'text/html');
32+
responseHeaders.set('Content-Security-Policy', header);
33+
34+
// Add the document policy header to enable JS profiling
35+
// This is required for Sentry's profiling integration
36+
responseHeaders.set('Document-Policy', 'js-profiling');
37+
38+
return new Response(body, {
39+
headers: responseHeaders,
40+
status: responseStatusCode,
41+
});
42+
}
43+
44+
export const handleError: HandleErrorFunction = (error, { request }) => {
45+
// React Router may abort some interrupted requests, don't log those
46+
if (!request.signal.aborted) {
47+
Sentry.captureException(error);
48+
// optionally log the error so you can see it
49+
console.error(error);
50+
}
51+
};
52+
53+
54+
export default Sentry.wrapSentryHandleRequest(handleRequest);
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// NOTE: https://shopify.dev/docs/api/storefront/latest/queries/cart
2+
export const CART_QUERY_FRAGMENT = `#graphql
3+
fragment Money on MoneyV2 {
4+
currencyCode
5+
amount
6+
}
7+
fragment CartLine on CartLine {
8+
id
9+
quantity
10+
attributes {
11+
key
12+
value
13+
}
14+
cost {
15+
totalAmount {
16+
...Money
17+
}
18+
amountPerQuantity {
19+
...Money
20+
}
21+
compareAtAmountPerQuantity {
22+
...Money
23+
}
24+
}
25+
merchandise {
26+
... on ProductVariant {
27+
id
28+
availableForSale
29+
compareAtPrice {
30+
...Money
31+
}
32+
price {
33+
...Money
34+
}
35+
requiresShipping
36+
title
37+
image {
38+
id
39+
url
40+
altText
41+
width
42+
height
43+
44+
}
45+
product {
46+
handle
47+
title
48+
id
49+
vendor
50+
}
51+
selectedOptions {
52+
name
53+
value
54+
}
55+
}
56+
}
57+
}
58+
fragment CartApiQuery on Cart {
59+
updatedAt
60+
id
61+
checkoutUrl
62+
totalQuantity
63+
buyerIdentity {
64+
countryCode
65+
customer {
66+
id
67+
email
68+
firstName
69+
lastName
70+
displayName
71+
}
72+
email
73+
phone
74+
}
75+
lines(first: $numCartLines) {
76+
nodes {
77+
...CartLine
78+
}
79+
}
80+
cost {
81+
subtotalAmount {
82+
...Money
83+
}
84+
totalAmount {
85+
...Money
86+
}
87+
totalDutyAmount {
88+
...Money
89+
}
90+
totalTaxAmount {
91+
...Money
92+
}
93+
}
94+
note
95+
attributes {
96+
key
97+
value
98+
}
99+
discountCodes {
100+
code
101+
applicable
102+
}
103+
}
104+
` as const;
105+
106+
const MENU_FRAGMENT = `#graphql
107+
fragment MenuItem on MenuItem {
108+
id
109+
resourceId
110+
tags
111+
title
112+
type
113+
url
114+
}
115+
fragment ChildMenuItem on MenuItem {
116+
...MenuItem
117+
}
118+
fragment ParentMenuItem on MenuItem {
119+
...MenuItem
120+
items {
121+
...ChildMenuItem
122+
}
123+
}
124+
fragment Menu on Menu {
125+
id
126+
items {
127+
...ParentMenuItem
128+
}
129+
}
130+
` as const;
131+
132+
export const HEADER_QUERY = `#graphql
133+
fragment Shop on Shop {
134+
id
135+
name
136+
description
137+
primaryDomain {
138+
url
139+
}
140+
brand {
141+
logo {
142+
image {
143+
url
144+
}
145+
}
146+
}
147+
}
148+
query Header(
149+
$country: CountryCode
150+
$headerMenuHandle: String!
151+
$language: LanguageCode
152+
) @inContext(language: $language, country: $country) {
153+
shop {
154+
...Shop
155+
}
156+
menu(handle: $headerMenuHandle) {
157+
...Menu
158+
}
159+
}
160+
${MENU_FRAGMENT}
161+
` as const;
162+
163+
export const FOOTER_QUERY = `#graphql
164+
query Footer(
165+
$country: CountryCode
166+
$footerMenuHandle: String!
167+
$language: LanguageCode
168+
) @inContext(language: $language, country: $country) {
169+
menu(handle: $footerMenuHandle) {
170+
...Menu
171+
}
172+
}
173+
${MENU_FRAGMENT}
174+
` as const;

0 commit comments

Comments
 (0)