Skip to content

Commit 6b1e7b6

Browse files
committed
feat(remix): Add support for Hydrogen
1 parent 6227c13 commit 6b1e7b6

Some content is hidden

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

51 files changed

+2218
-255
lines changed
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,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+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
node_modules
2+
/.cache
3+
/build
4+
/dist
5+
/public/build
6+
/.mf
7+
.env
8+
.shopify
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { RemixBrowser, useLocation, useMatches } from '@remix-run/react';
2+
import * as Sentry from '@sentry/remix/cloudflare';
3+
import { StrictMode, startTransition } from 'react';
4+
import { useEffect } from 'react';
5+
import { hydrateRoot } from 'react-dom/client';
6+
7+
Sentry.init({
8+
environment: 'qa', // dynamic sampling bias to keep transactions
9+
// Could not find a working way to set the DSN in the browser side from the environment variables
10+
dsn: 'https://[email protected]/1337',
11+
debug: true,
12+
integrations: [
13+
Sentry.browserTracingIntegration({
14+
useEffect,
15+
useLocation,
16+
useMatches,
17+
}),
18+
Sentry.replayIntegration({
19+
maskAllText: true,
20+
blockAllMedia: true,
21+
}),
22+
],
23+
24+
tracesSampleRate: 1.0,
25+
replaysSessionSampleRate: 0.1,
26+
replaysOnErrorSampleRate: 1.0,
27+
tunnel: 'http://localhost:3031/', // proxy server
28+
});
29+
30+
startTransition(() => {
31+
hydrateRoot(
32+
document,
33+
<StrictMode>
34+
<RemixBrowser />
35+
</StrictMode>,
36+
);
37+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { RemixServer } from '@remix-run/react';
2+
import { createContentSecurityPolicy } from '@shopify/hydrogen';
3+
import type { EntryContext } from '@shopify/remix-oxygen';
4+
import isbot from 'isbot';
5+
import { renderToReadableStream } from 'react-dom/server';
6+
7+
export default async function handleRequest(
8+
request: Request,
9+
responseStatusCode: number,
10+
responseHeaders: Headers,
11+
remixContext: EntryContext,
12+
) {
13+
const { nonce, header, NonceProvider } = createContentSecurityPolicy({
14+
connectSrc: [
15+
// Need to allow the proxy server to fetch the data
16+
'http://localhost:3031/',
17+
],
18+
});
19+
20+
const body = await renderToReadableStream(
21+
<NonceProvider>
22+
<RemixServer context={remixContext} url={request.url} />
23+
</NonceProvider>,
24+
{
25+
nonce,
26+
signal: request.signal,
27+
onError(error) {
28+
// eslint-disable-next-line no-console
29+
console.error(error);
30+
responseStatusCode = 500;
31+
},
32+
},
33+
);
34+
35+
if (isbot(request.headers.get('user-agent'))) {
36+
await body.allReady;
37+
}
38+
39+
responseHeaders.set('Content-Type', 'text/html');
40+
responseHeaders.set('Content-Security-Policy', header);
41+
42+
// Add the document policy header to enable JS profiling
43+
// This is required for Sentry's profiling integration
44+
responseHeaders.set('Document-Policy', 'js-profiling');
45+
46+
return new Response(body, {
47+
headers: responseHeaders,
48+
status: responseStatusCode,
49+
});
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { createPagesFunctionHandler } from '@remix-run/cloudflare-pages';
2+
import { sentryPagesPlugin } from '@sentry/cloudflare';
3+
4+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
5+
// @ts-ignore - the server build file is generated by `remix vite:build`
6+
// eslint-disable-next-line import/no-unresolved
7+
import * as build from '../build/server';
8+
9+
export const onRequest = [
10+
context => sentryPagesPlugin({ dsn: context.env.E2E_TEST_DSN, tracesSampleRate: 1.0 })(context),
11+
createPagesFunctionHandler({ build }),
12+
];
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)