hydrogen-forge 0.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.
- package/README.md +212 -0
- package/dist/commands/add.d.ts +7 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +123 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/create.d.ts +8 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +160 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/setup-mcp.d.ts +7 -0
- package/dist/commands/setup-mcp.d.ts.map +1 -0
- package/dist/commands/setup-mcp.js +179 -0
- package/dist/commands/setup-mcp.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +50 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/generators.d.ts +6 -0
- package/dist/lib/generators.d.ts.map +1 -0
- package/dist/lib/generators.js +470 -0
- package/dist/lib/generators.js.map +1 -0
- package/dist/lib/utils.d.ts +17 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +101 -0
- package/dist/lib/utils.js.map +1 -0
- package/package.json +54 -0
- package/templates/starter/.env.example +21 -0
- package/templates/starter/.graphqlrc.ts +27 -0
- package/templates/starter/README.md +117 -0
- package/templates/starter/app/assets/favicon.svg +28 -0
- package/templates/starter/app/components/AddToCartButton.tsx +102 -0
- package/templates/starter/app/components/Aside.tsx +136 -0
- package/templates/starter/app/components/CartLineItem.tsx +229 -0
- package/templates/starter/app/components/CartMain.tsx +131 -0
- package/templates/starter/app/components/CartSummary.tsx +315 -0
- package/templates/starter/app/components/CollectionFilters.tsx +330 -0
- package/templates/starter/app/components/CollectionGrid.tsx +141 -0
- package/templates/starter/app/components/Footer.tsx +218 -0
- package/templates/starter/app/components/Header.tsx +296 -0
- package/templates/starter/app/components/PageLayout.tsx +174 -0
- package/templates/starter/app/components/PaginatedResourceSection.tsx +41 -0
- package/templates/starter/app/components/ProductCard.tsx +151 -0
- package/templates/starter/app/components/ProductForm.tsx +156 -0
- package/templates/starter/app/components/ProductGallery.tsx +164 -0
- package/templates/starter/app/components/ProductGrid.tsx +64 -0
- package/templates/starter/app/components/ProductImage.tsx +23 -0
- package/templates/starter/app/components/ProductItem.tsx +44 -0
- package/templates/starter/app/components/ProductPrice.tsx +97 -0
- package/templates/starter/app/components/SearchDialog.tsx +599 -0
- package/templates/starter/app/components/SearchForm.tsx +68 -0
- package/templates/starter/app/components/SearchFormPredictive.tsx +76 -0
- package/templates/starter/app/components/SearchResults.tsx +161 -0
- package/templates/starter/app/components/SearchResultsPredictive.tsx +461 -0
- package/templates/starter/app/entry.client.tsx +21 -0
- package/templates/starter/app/entry.server.tsx +53 -0
- package/templates/starter/app/graphql/customer-account/CustomerAddressMutations.ts +64 -0
- package/templates/starter/app/graphql/customer-account/CustomerDetailsQuery.ts +40 -0
- package/templates/starter/app/graphql/customer-account/CustomerOrderQuery.ts +90 -0
- package/templates/starter/app/graphql/customer-account/CustomerOrdersQuery.ts +63 -0
- package/templates/starter/app/graphql/customer-account/CustomerUpdateMutation.ts +25 -0
- package/templates/starter/app/lib/context.ts +60 -0
- package/templates/starter/app/lib/fragments.ts +234 -0
- package/templates/starter/app/lib/orderFilters.ts +90 -0
- package/templates/starter/app/lib/redirect.ts +23 -0
- package/templates/starter/app/lib/search.ts +79 -0
- package/templates/starter/app/lib/session.ts +72 -0
- package/templates/starter/app/lib/variants.ts +46 -0
- package/templates/starter/app/root.tsx +209 -0
- package/templates/starter/app/routes/$.tsx +11 -0
- package/templates/starter/app/routes/[robots.txt].tsx +117 -0
- package/templates/starter/app/routes/[sitemap.xml].tsx +16 -0
- package/templates/starter/app/routes/_index.tsx +167 -0
- package/templates/starter/app/routes/account.$.tsx +9 -0
- package/templates/starter/app/routes/account._index.tsx +5 -0
- package/templates/starter/app/routes/account.addresses.tsx +516 -0
- package/templates/starter/app/routes/account.orders.$id.tsx +222 -0
- package/templates/starter/app/routes/account.orders._index.tsx +222 -0
- package/templates/starter/app/routes/account.profile.tsx +133 -0
- package/templates/starter/app/routes/account.tsx +97 -0
- package/templates/starter/app/routes/account_.authorize.tsx +5 -0
- package/templates/starter/app/routes/account_.login.tsx +7 -0
- package/templates/starter/app/routes/account_.logout.tsx +11 -0
- package/templates/starter/app/routes/api.$version.[graphql.json].tsx +14 -0
- package/templates/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +129 -0
- package/templates/starter/app/routes/blogs.$blogHandle._index.tsx +175 -0
- package/templates/starter/app/routes/blogs._index.tsx +109 -0
- package/templates/starter/app/routes/cart.$lines.tsx +70 -0
- package/templates/starter/app/routes/cart.tsx +117 -0
- package/templates/starter/app/routes/collections.$handle.tsx +161 -0
- package/templates/starter/app/routes/collections._index.tsx +133 -0
- package/templates/starter/app/routes/collections.all.tsx +122 -0
- package/templates/starter/app/routes/discount.$code.tsx +48 -0
- package/templates/starter/app/routes/pages.$handle.tsx +88 -0
- package/templates/starter/app/routes/policies.$handle.tsx +93 -0
- package/templates/starter/app/routes/policies._index.tsx +69 -0
- package/templates/starter/app/routes/products.$handle.tsx +232 -0
- package/templates/starter/app/routes/search.tsx +426 -0
- package/templates/starter/app/routes/sitemap.$type.$page[.xml].tsx +23 -0
- package/templates/starter/app/routes.ts +9 -0
- package/templates/starter/app/styles/app.css +574 -0
- package/templates/starter/app/styles/reset.css +139 -0
- package/templates/starter/app/styles/tailwind.css +116 -0
- package/templates/starter/customer-accountapi.generated.d.ts +543 -0
- package/templates/starter/env.d.ts +7 -0
- package/templates/starter/eslint.config.js +247 -0
- package/templates/starter/guides/predictiveSearch/predictiveSearch.jpg +0 -0
- package/templates/starter/guides/predictiveSearch/predictiveSearch.md +394 -0
- package/templates/starter/guides/search/search.jpg +0 -0
- package/templates/starter/guides/search/search.md +335 -0
- package/templates/starter/package.json +71 -0
- package/templates/starter/postcss.config.js +6 -0
- package/templates/starter/public/.gitkeep +0 -0
- package/templates/starter/react-router.config.ts +13 -0
- package/templates/starter/server.ts +59 -0
- package/templates/starter/storefrontapi.generated.d.ts +1264 -0
- package/templates/starter/tailwind.config.js +83 -0
- package/templates/starter/tsconfig.json +67 -0
- package/templates/starter/vite.config.ts +32 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# Hydrogen Search
|
|
2
|
+
|
|
3
|
+
Our skeleton template ships with a `/search` route and a set of components to easily
|
|
4
|
+
implement a traditional search flow.
|
|
5
|
+
|
|
6
|
+
This integration uses the storefront API (SFAPI) [search](https://shopify.dev/docs/api/storefront/latest/queries/search)
|
|
7
|
+
endpoint to retrieve search results based on a search term.
|
|
8
|
+
|
|
9
|
+
## Components Architecture
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
## Components
|
|
14
|
+
|
|
15
|
+
| File | Description |
|
|
16
|
+
| ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
17
|
+
| [`app/components/SearchForm.tsx`](app/components/SearchForm.tsx) | A fully customizable form component configured to make (server-side) form `GET` requests to the `/search` route. |
|
|
18
|
+
| [`app/components/SearchResults.tsx`](app/components/SearchResults.tsx) | A fully customizable search results wrapper, that provides compound components to render `articles`, `pages` and `products` |
|
|
19
|
+
|
|
20
|
+
## Instructions
|
|
21
|
+
|
|
22
|
+
### 1. Create the search route
|
|
23
|
+
|
|
24
|
+
Create a new file at `/routes/search.tsx`
|
|
25
|
+
|
|
26
|
+
### 3. Add `search` query and fetcher
|
|
27
|
+
|
|
28
|
+
The search fetcher parses the `q` parameter and performs the search SFAPI request.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
/**
|
|
32
|
+
* Regular search query and fragments
|
|
33
|
+
* (adjust as needed)
|
|
34
|
+
*/
|
|
35
|
+
const SEARCH_PRODUCT_FRAGMENT = `#graphql
|
|
36
|
+
fragment SearchProduct on Product {
|
|
37
|
+
__typename
|
|
38
|
+
handle
|
|
39
|
+
id
|
|
40
|
+
publishedAt
|
|
41
|
+
title
|
|
42
|
+
trackingParameters
|
|
43
|
+
vendor
|
|
44
|
+
selectedOrFirstAvailableVariant(
|
|
45
|
+
selectedOptions: []
|
|
46
|
+
ignoreUnknownOptions: true
|
|
47
|
+
caseInsensitiveMatch: true
|
|
48
|
+
) {
|
|
49
|
+
id
|
|
50
|
+
image {
|
|
51
|
+
url
|
|
52
|
+
altText
|
|
53
|
+
width
|
|
54
|
+
height
|
|
55
|
+
}
|
|
56
|
+
price {
|
|
57
|
+
amount
|
|
58
|
+
currencyCode
|
|
59
|
+
}
|
|
60
|
+
compareAtPrice {
|
|
61
|
+
amount
|
|
62
|
+
currencyCode
|
|
63
|
+
}
|
|
64
|
+
selectedOptions {
|
|
65
|
+
name
|
|
66
|
+
value
|
|
67
|
+
}
|
|
68
|
+
product {
|
|
69
|
+
handle
|
|
70
|
+
title
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
` as const;
|
|
75
|
+
|
|
76
|
+
const SEARCH_PAGE_FRAGMENT = `#graphql
|
|
77
|
+
fragment SearchPage on Page {
|
|
78
|
+
__typename
|
|
79
|
+
handle
|
|
80
|
+
id
|
|
81
|
+
title
|
|
82
|
+
trackingParameters
|
|
83
|
+
}
|
|
84
|
+
` as const;
|
|
85
|
+
|
|
86
|
+
const SEARCH_ARTICLE_FRAGMENT = `#graphql
|
|
87
|
+
fragment SearchArticle on Article {
|
|
88
|
+
__typename
|
|
89
|
+
handle
|
|
90
|
+
id
|
|
91
|
+
title
|
|
92
|
+
trackingParameters
|
|
93
|
+
}
|
|
94
|
+
` as const;
|
|
95
|
+
|
|
96
|
+
const PAGE_INFO_FRAGMENT = `#graphql
|
|
97
|
+
fragment PageInfoFragment on PageInfo {
|
|
98
|
+
hasNextPage
|
|
99
|
+
hasPreviousPage
|
|
100
|
+
startCursor
|
|
101
|
+
endCursor
|
|
102
|
+
}
|
|
103
|
+
` as const;
|
|
104
|
+
|
|
105
|
+
// NOTE: https://shopify.dev/docs/api/storefront/latest/queries/search
|
|
106
|
+
export const SEARCH_QUERY = `#graphql
|
|
107
|
+
query Search(
|
|
108
|
+
$country: CountryCode
|
|
109
|
+
$endCursor: String
|
|
110
|
+
$first: Int
|
|
111
|
+
$language: LanguageCode
|
|
112
|
+
$last: Int
|
|
113
|
+
$term: String!
|
|
114
|
+
$startCursor: String
|
|
115
|
+
) @inContext(country: $country, language: $language) {
|
|
116
|
+
articles: search(
|
|
117
|
+
query: $term,
|
|
118
|
+
types: [ARTICLE],
|
|
119
|
+
first: $first,
|
|
120
|
+
) {
|
|
121
|
+
nodes {
|
|
122
|
+
...on Article {
|
|
123
|
+
...SearchArticle
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
pages: search(
|
|
128
|
+
query: $term,
|
|
129
|
+
types: [PAGE],
|
|
130
|
+
first: $first,
|
|
131
|
+
) {
|
|
132
|
+
nodes {
|
|
133
|
+
...on Page {
|
|
134
|
+
...SearchPage
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
products: search(
|
|
139
|
+
after: $endCursor,
|
|
140
|
+
before: $startCursor,
|
|
141
|
+
first: $first,
|
|
142
|
+
last: $last,
|
|
143
|
+
query: $term,
|
|
144
|
+
sortKey: RELEVANCE,
|
|
145
|
+
types: [PRODUCT],
|
|
146
|
+
unavailableProducts: HIDE,
|
|
147
|
+
) {
|
|
148
|
+
nodes {
|
|
149
|
+
...on Product {
|
|
150
|
+
...SearchProduct
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
pageInfo {
|
|
154
|
+
...PageInfoFragment
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
${SEARCH_PRODUCT_FRAGMENT}
|
|
159
|
+
${SEARCH_PAGE_FRAGMENT}
|
|
160
|
+
${SEARCH_ARTICLE_FRAGMENT}
|
|
161
|
+
${PAGE_INFO_FRAGMENT}
|
|
162
|
+
` as const;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Regular search fetcher
|
|
166
|
+
*/
|
|
167
|
+
async function search({
|
|
168
|
+
request,
|
|
169
|
+
context,
|
|
170
|
+
}: Pick<LoaderFunctionArgs, 'request' | 'context'>) {
|
|
171
|
+
const {storefront} = context;
|
|
172
|
+
const url = new URL(request.url);
|
|
173
|
+
const searchParams = new URLSearchParams(url.search);
|
|
174
|
+
const variables = getPaginationVariables(request, {pageBy: 8});
|
|
175
|
+
const term = String(searchParams.get('q') || '');
|
|
176
|
+
|
|
177
|
+
// Search articles, pages, and products for the `q` term
|
|
178
|
+
const {errors, ...items} = await storefront.query(SEARCH_QUERY, {
|
|
179
|
+
variables: {...variables, term},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (!items) {
|
|
183
|
+
throw new Error('No search data returned from Shopify API');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (errors) {
|
|
187
|
+
throw new Error(errors[0].message);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const total = Object.values(items).reduce((acc, {nodes}) => {
|
|
191
|
+
return acc + nodes.length;
|
|
192
|
+
}, 0);
|
|
193
|
+
|
|
194
|
+
return {term, result: {total, items}};
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### 3. Add a `loader` export to the route
|
|
199
|
+
|
|
200
|
+
This loader receives and processes `GET` requests from the `<SearchForm />` component.
|
|
201
|
+
|
|
202
|
+
A `q` URL parameter will be used as the search term and appended automatically by
|
|
203
|
+
the form if present in it's children prop
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
/**
|
|
207
|
+
* Handles regular search GET requests
|
|
208
|
+
* requested by the SearchForm component and /search route visits
|
|
209
|
+
*/
|
|
210
|
+
export async function loader({request, context}: LoaderFunctionArgs) {
|
|
211
|
+
const url = new URL(request.url);
|
|
212
|
+
const isRegular = !url.searchParams.has('predictive');
|
|
213
|
+
|
|
214
|
+
if (!isRegular) {
|
|
215
|
+
return {};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const searchPromise = regularSearch({request, context});
|
|
219
|
+
|
|
220
|
+
searchPromise.catch((error: Error) => {
|
|
221
|
+
console.error(error);
|
|
222
|
+
return {term: '', result: null, error: error.message};
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return await searchPromise;
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### 4. Render the search form and results
|
|
230
|
+
|
|
231
|
+
Finally, create a default export to render both the search form and the search results
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
import {SearchForm} from '~/components/SearchForm';
|
|
235
|
+
import {SearchResults} from '~/components/SearchResults';
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Renders the /search route
|
|
239
|
+
*/
|
|
240
|
+
export default function SearchPage() {
|
|
241
|
+
const {term, result} = useLoaderData<typeof loader>();
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<div className="search">
|
|
245
|
+
<h1>Search</h1>
|
|
246
|
+
<SearchForm>
|
|
247
|
+
{({inputRef}) => (
|
|
248
|
+
<>
|
|
249
|
+
<input
|
|
250
|
+
defaultValue={term}
|
|
251
|
+
name="q"
|
|
252
|
+
placeholder="Search…"
|
|
253
|
+
ref={inputRef}
|
|
254
|
+
type="search"
|
|
255
|
+
/>
|
|
256
|
+
|
|
257
|
+
<button type="submit">Search</button>
|
|
258
|
+
</>
|
|
259
|
+
)}
|
|
260
|
+
</SearchForm>
|
|
261
|
+
{!term || !result?.total ? (
|
|
262
|
+
<SearchResults.Empty />
|
|
263
|
+
) : (
|
|
264
|
+
<SearchResults result={result} term={term}>
|
|
265
|
+
{({articles, pages, products, term}) => (
|
|
266
|
+
<div>
|
|
267
|
+
<SearchResults.Products products={products} term={term} />
|
|
268
|
+
<SearchResults.Pages pages={pages} term={term} />
|
|
269
|
+
<SearchResults.Articles articles={articles} term={term} />
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
</SearchResults>
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Additional Notes
|
|
280
|
+
|
|
281
|
+
### How to use a different URL search parameter?
|
|
282
|
+
|
|
283
|
+
- Modify the `name` attribute in the forms input element. e.g
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
<input name="query" />`.
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
- Modify the search fetcher term variable to parse the new name. e.g
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
const term = String(searchParams.get('query') || '');
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### How to customize the way the results look?
|
|
296
|
+
|
|
297
|
+
Simply go to `/app/components/SearchResults.txx` and look for the compound component you
|
|
298
|
+
want to modify.
|
|
299
|
+
|
|
300
|
+
For example, let's render articles in a horizontal flex container
|
|
301
|
+
|
|
302
|
+
```diff
|
|
303
|
+
SearchResults.Pages = function({
|
|
304
|
+
pages,
|
|
305
|
+
term,
|
|
306
|
+
}: {
|
|
307
|
+
pages: SearchItems['pages'];
|
|
308
|
+
term: string;
|
|
309
|
+
}) {
|
|
310
|
+
if (!pages?.nodes.length) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
return (
|
|
314
|
+
<div className="search-result">
|
|
315
|
+
<h2>Pages</h2>
|
|
316
|
+
+ <div className="flex">
|
|
317
|
+
{pages?.nodes?.map((page) => {
|
|
318
|
+
const pageUrl = urlWithTrackingParams({
|
|
319
|
+
baseUrl: `/pages/${page.handle}`,
|
|
320
|
+
trackingParams: page.trackingParameters,
|
|
321
|
+
term,
|
|
322
|
+
});
|
|
323
|
+
return (
|
|
324
|
+
<div className="search-results-item" key={page.id}>
|
|
325
|
+
<Link prefetch="intent" to={pageUrl}>
|
|
326
|
+
{page.title}
|
|
327
|
+
</Link>
|
|
328
|
+
</div>
|
|
329
|
+
);
|
|
330
|
+
})}
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
);
|
|
334
|
+
};
|
|
335
|
+
```
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hydrogen-forge/starter",
|
|
3
|
+
"private": true,
|
|
4
|
+
"sideEffects": false,
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"description": "Hydrogen Forge starter theme - developer-focused Shopify Hydrogen template",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "shopify hydrogen build --codegen",
|
|
10
|
+
"dev": "shopify hydrogen dev --codegen",
|
|
11
|
+
"preview": "shopify hydrogen preview --build",
|
|
12
|
+
"lint": "eslint --no-error-on-unmatched-pattern .",
|
|
13
|
+
"typecheck": "react-router typegen && tsc --noEmit",
|
|
14
|
+
"codegen": "shopify hydrogen codegen && react-router typegen",
|
|
15
|
+
"format": "prettier --write .",
|
|
16
|
+
"format:check": "prettier --check ."
|
|
17
|
+
},
|
|
18
|
+
"prettier": "@shopify/prettier-config",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@shopify/hydrogen": "2025.7.1",
|
|
21
|
+
"clsx": "^2.1.0",
|
|
22
|
+
"graphql": "^16.10.0",
|
|
23
|
+
"graphql-tag": "^2.12.6",
|
|
24
|
+
"isbot": "^5.1.22",
|
|
25
|
+
"react": "18.3.1",
|
|
26
|
+
"react-dom": "18.3.1",
|
|
27
|
+
"react-router": "^7.9.2",
|
|
28
|
+
"react-router-dom": "^7.9.2"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@eslint/compat": "^1.2.5",
|
|
32
|
+
"@eslint/eslintrc": "^3.2.0",
|
|
33
|
+
"@eslint/js": "^9.18.0",
|
|
34
|
+
"@graphql-codegen/cli": "5.0.2",
|
|
35
|
+
"@react-router/dev": "^7.9.2",
|
|
36
|
+
"@react-router/fs-routes": "^7.9.2",
|
|
37
|
+
"@shopify/cli": "3.85.4",
|
|
38
|
+
"@shopify/hydrogen-codegen": "^0.3.3",
|
|
39
|
+
"@shopify/mini-oxygen": "^4.0.0",
|
|
40
|
+
"@shopify/oxygen-workers-types": "^4.1.6",
|
|
41
|
+
"@shopify/prettier-config": "^1.1.2",
|
|
42
|
+
"@tailwindcss/forms": "^0.5.7",
|
|
43
|
+
"@tailwindcss/typography": "^0.5.10",
|
|
44
|
+
"@total-typescript/ts-reset": "^0.6.1",
|
|
45
|
+
"@types/eslint": "^9.6.1",
|
|
46
|
+
"@types/react": "^18.2.22",
|
|
47
|
+
"@types/react-dom": "^18.2.7",
|
|
48
|
+
"@typescript-eslint/eslint-plugin": "^8.21.0",
|
|
49
|
+
"@typescript-eslint/parser": "^8.21.0",
|
|
50
|
+
"autoprefixer": "^10.4.17",
|
|
51
|
+
"eslint": "^9.18.0",
|
|
52
|
+
"eslint-config-prettier": "^10.0.1",
|
|
53
|
+
"eslint-import-resolver-typescript": "^3.7.0",
|
|
54
|
+
"eslint-plugin-eslint-comments": "^3.2.0",
|
|
55
|
+
"eslint-plugin-import": "^2.31.0",
|
|
56
|
+
"eslint-plugin-jest": "^28.11.0",
|
|
57
|
+
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
58
|
+
"eslint-plugin-react": "^7.37.4",
|
|
59
|
+
"eslint-plugin-react-hooks": "^5.1.0",
|
|
60
|
+
"globals": "^15.14.0",
|
|
61
|
+
"postcss": "^8.4.35",
|
|
62
|
+
"prettier": "^3.4.2",
|
|
63
|
+
"tailwindcss": "^3.4.1",
|
|
64
|
+
"typescript": "^5.9.2",
|
|
65
|
+
"vite": "^6.2.4",
|
|
66
|
+
"vite-tsconfig-paths": "^4.3.1"
|
|
67
|
+
},
|
|
68
|
+
"engines": {
|
|
69
|
+
"node": ">=20.0.0"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type {Config} from '@react-router/dev/config';
|
|
2
|
+
import {hydrogenPreset} from '@shopify/hydrogen/react-router-preset';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* React Router 7.9.x Configuration for Hydrogen
|
|
6
|
+
*
|
|
7
|
+
* This configuration uses the official Hydrogen preset to provide optimal
|
|
8
|
+
* React Router settings for Shopify Oxygen deployment. The preset enables
|
|
9
|
+
* validated performance optimizations while ensuring compatibility.
|
|
10
|
+
*/
|
|
11
|
+
export default {
|
|
12
|
+
presets: [hydrogenPreset()],
|
|
13
|
+
} satisfies Config;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as serverBuild from 'virtual:react-router/server-build';
|
|
2
|
+
import {createRequestHandler, storefrontRedirect} from '@shopify/hydrogen';
|
|
3
|
+
import {createHydrogenRouterContext} from '~/lib/context';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Export a fetch handler in module format.
|
|
7
|
+
*/
|
|
8
|
+
export default {
|
|
9
|
+
async fetch(
|
|
10
|
+
request: Request,
|
|
11
|
+
env: Env,
|
|
12
|
+
executionContext: ExecutionContext,
|
|
13
|
+
): Promise<Response> {
|
|
14
|
+
try {
|
|
15
|
+
const hydrogenContext = await createHydrogenRouterContext(
|
|
16
|
+
request,
|
|
17
|
+
env,
|
|
18
|
+
executionContext,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create a Hydrogen request handler that internally
|
|
23
|
+
* delegates to React Router for routing and rendering.
|
|
24
|
+
*/
|
|
25
|
+
const handleRequest = createRequestHandler({
|
|
26
|
+
build: serverBuild,
|
|
27
|
+
mode: process.env.NODE_ENV,
|
|
28
|
+
getLoadContext: () => hydrogenContext,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const response = await handleRequest(request);
|
|
32
|
+
|
|
33
|
+
if (hydrogenContext.session.isPending) {
|
|
34
|
+
response.headers.set(
|
|
35
|
+
'Set-Cookie',
|
|
36
|
+
await hydrogenContext.session.commit(),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (response.status === 404) {
|
|
41
|
+
/**
|
|
42
|
+
* Check for redirects only when there's a 404 from the app.
|
|
43
|
+
* If the redirect doesn't exist, then `storefrontRedirect`
|
|
44
|
+
* will pass through the 404 response.
|
|
45
|
+
*/
|
|
46
|
+
return storefrontRedirect({
|
|
47
|
+
request,
|
|
48
|
+
response,
|
|
49
|
+
storefront: hydrogenContext.storefront,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return response;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error(error);
|
|
56
|
+
return new Response('An unexpected error occurred', {status: 500});
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|