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.
Files changed (118) hide show
  1. package/README.md +212 -0
  2. package/dist/commands/add.d.ts +7 -0
  3. package/dist/commands/add.d.ts.map +1 -0
  4. package/dist/commands/add.js +123 -0
  5. package/dist/commands/add.js.map +1 -0
  6. package/dist/commands/create.d.ts +8 -0
  7. package/dist/commands/create.d.ts.map +1 -0
  8. package/dist/commands/create.js +160 -0
  9. package/dist/commands/create.js.map +1 -0
  10. package/dist/commands/setup-mcp.d.ts +7 -0
  11. package/dist/commands/setup-mcp.d.ts.map +1 -0
  12. package/dist/commands/setup-mcp.js +179 -0
  13. package/dist/commands/setup-mcp.js.map +1 -0
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +50 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/lib/generators.d.ts +6 -0
  19. package/dist/lib/generators.d.ts.map +1 -0
  20. package/dist/lib/generators.js +470 -0
  21. package/dist/lib/generators.js.map +1 -0
  22. package/dist/lib/utils.d.ts +17 -0
  23. package/dist/lib/utils.d.ts.map +1 -0
  24. package/dist/lib/utils.js +101 -0
  25. package/dist/lib/utils.js.map +1 -0
  26. package/package.json +54 -0
  27. package/templates/starter/.env.example +21 -0
  28. package/templates/starter/.graphqlrc.ts +27 -0
  29. package/templates/starter/README.md +117 -0
  30. package/templates/starter/app/assets/favicon.svg +28 -0
  31. package/templates/starter/app/components/AddToCartButton.tsx +102 -0
  32. package/templates/starter/app/components/Aside.tsx +136 -0
  33. package/templates/starter/app/components/CartLineItem.tsx +229 -0
  34. package/templates/starter/app/components/CartMain.tsx +131 -0
  35. package/templates/starter/app/components/CartSummary.tsx +315 -0
  36. package/templates/starter/app/components/CollectionFilters.tsx +330 -0
  37. package/templates/starter/app/components/CollectionGrid.tsx +141 -0
  38. package/templates/starter/app/components/Footer.tsx +218 -0
  39. package/templates/starter/app/components/Header.tsx +296 -0
  40. package/templates/starter/app/components/PageLayout.tsx +174 -0
  41. package/templates/starter/app/components/PaginatedResourceSection.tsx +41 -0
  42. package/templates/starter/app/components/ProductCard.tsx +151 -0
  43. package/templates/starter/app/components/ProductForm.tsx +156 -0
  44. package/templates/starter/app/components/ProductGallery.tsx +164 -0
  45. package/templates/starter/app/components/ProductGrid.tsx +64 -0
  46. package/templates/starter/app/components/ProductImage.tsx +23 -0
  47. package/templates/starter/app/components/ProductItem.tsx +44 -0
  48. package/templates/starter/app/components/ProductPrice.tsx +97 -0
  49. package/templates/starter/app/components/SearchDialog.tsx +599 -0
  50. package/templates/starter/app/components/SearchForm.tsx +68 -0
  51. package/templates/starter/app/components/SearchFormPredictive.tsx +76 -0
  52. package/templates/starter/app/components/SearchResults.tsx +161 -0
  53. package/templates/starter/app/components/SearchResultsPredictive.tsx +461 -0
  54. package/templates/starter/app/entry.client.tsx +21 -0
  55. package/templates/starter/app/entry.server.tsx +53 -0
  56. package/templates/starter/app/graphql/customer-account/CustomerAddressMutations.ts +64 -0
  57. package/templates/starter/app/graphql/customer-account/CustomerDetailsQuery.ts +40 -0
  58. package/templates/starter/app/graphql/customer-account/CustomerOrderQuery.ts +90 -0
  59. package/templates/starter/app/graphql/customer-account/CustomerOrdersQuery.ts +63 -0
  60. package/templates/starter/app/graphql/customer-account/CustomerUpdateMutation.ts +25 -0
  61. package/templates/starter/app/lib/context.ts +60 -0
  62. package/templates/starter/app/lib/fragments.ts +234 -0
  63. package/templates/starter/app/lib/orderFilters.ts +90 -0
  64. package/templates/starter/app/lib/redirect.ts +23 -0
  65. package/templates/starter/app/lib/search.ts +79 -0
  66. package/templates/starter/app/lib/session.ts +72 -0
  67. package/templates/starter/app/lib/variants.ts +46 -0
  68. package/templates/starter/app/root.tsx +209 -0
  69. package/templates/starter/app/routes/$.tsx +11 -0
  70. package/templates/starter/app/routes/[robots.txt].tsx +117 -0
  71. package/templates/starter/app/routes/[sitemap.xml].tsx +16 -0
  72. package/templates/starter/app/routes/_index.tsx +167 -0
  73. package/templates/starter/app/routes/account.$.tsx +9 -0
  74. package/templates/starter/app/routes/account._index.tsx +5 -0
  75. package/templates/starter/app/routes/account.addresses.tsx +516 -0
  76. package/templates/starter/app/routes/account.orders.$id.tsx +222 -0
  77. package/templates/starter/app/routes/account.orders._index.tsx +222 -0
  78. package/templates/starter/app/routes/account.profile.tsx +133 -0
  79. package/templates/starter/app/routes/account.tsx +97 -0
  80. package/templates/starter/app/routes/account_.authorize.tsx +5 -0
  81. package/templates/starter/app/routes/account_.login.tsx +7 -0
  82. package/templates/starter/app/routes/account_.logout.tsx +11 -0
  83. package/templates/starter/app/routes/api.$version.[graphql.json].tsx +14 -0
  84. package/templates/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +129 -0
  85. package/templates/starter/app/routes/blogs.$blogHandle._index.tsx +175 -0
  86. package/templates/starter/app/routes/blogs._index.tsx +109 -0
  87. package/templates/starter/app/routes/cart.$lines.tsx +70 -0
  88. package/templates/starter/app/routes/cart.tsx +117 -0
  89. package/templates/starter/app/routes/collections.$handle.tsx +161 -0
  90. package/templates/starter/app/routes/collections._index.tsx +133 -0
  91. package/templates/starter/app/routes/collections.all.tsx +122 -0
  92. package/templates/starter/app/routes/discount.$code.tsx +48 -0
  93. package/templates/starter/app/routes/pages.$handle.tsx +88 -0
  94. package/templates/starter/app/routes/policies.$handle.tsx +93 -0
  95. package/templates/starter/app/routes/policies._index.tsx +69 -0
  96. package/templates/starter/app/routes/products.$handle.tsx +232 -0
  97. package/templates/starter/app/routes/search.tsx +426 -0
  98. package/templates/starter/app/routes/sitemap.$type.$page[.xml].tsx +23 -0
  99. package/templates/starter/app/routes.ts +9 -0
  100. package/templates/starter/app/styles/app.css +574 -0
  101. package/templates/starter/app/styles/reset.css +139 -0
  102. package/templates/starter/app/styles/tailwind.css +116 -0
  103. package/templates/starter/customer-accountapi.generated.d.ts +543 -0
  104. package/templates/starter/env.d.ts +7 -0
  105. package/templates/starter/eslint.config.js +247 -0
  106. package/templates/starter/guides/predictiveSearch/predictiveSearch.jpg +0 -0
  107. package/templates/starter/guides/predictiveSearch/predictiveSearch.md +394 -0
  108. package/templates/starter/guides/search/search.jpg +0 -0
  109. package/templates/starter/guides/search/search.md +335 -0
  110. package/templates/starter/package.json +71 -0
  111. package/templates/starter/postcss.config.js +6 -0
  112. package/templates/starter/public/.gitkeep +0 -0
  113. package/templates/starter/react-router.config.ts +13 -0
  114. package/templates/starter/server.ts +59 -0
  115. package/templates/starter/storefrontapi.generated.d.ts +1264 -0
  116. package/templates/starter/tailwind.config.js +83 -0
  117. package/templates/starter/tsconfig.json +67 -0
  118. 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
+ ![alt text](./search.jpg)
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
+ &nbsp;
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
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
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
+ };