@doswiftly/cli 0.1.24 → 0.2.1

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 (98) hide show
  1. package/dist/commands/check.js +2 -2
  2. package/dist/commands/deploy.d.ts.map +1 -1
  3. package/dist/commands/deploy.js +8 -5
  4. package/dist/commands/deploy.js.map +1 -1
  5. package/dist/commands/dev.d.ts +13 -0
  6. package/dist/commands/dev.d.ts.map +1 -1
  7. package/dist/commands/dev.js +155 -63
  8. package/dist/commands/dev.js.map +1 -1
  9. package/dist/commands/doctor.d.ts.map +1 -1
  10. package/dist/commands/doctor.js +3 -4
  11. package/dist/commands/doctor.js.map +1 -1
  12. package/dist/commands/init.d.ts.map +1 -1
  13. package/dist/commands/init.js +271 -166
  14. package/dist/commands/init.js.map +1 -1
  15. package/dist/commands/sdk.d.ts +1 -1
  16. package/dist/commands/sdk.js +3 -3
  17. package/dist/commands/sdk.js.map +1 -1
  18. package/dist/commands/template.d.ts.map +1 -1
  19. package/dist/commands/template.js +4 -31
  20. package/dist/commands/template.js.map +1 -1
  21. package/dist/commands/verify.js +5 -5
  22. package/dist/commands/verify.js.map +1 -1
  23. package/dist/index.js +2 -3
  24. package/dist/index.js.map +1 -1
  25. package/dist/lib/i18n.d.ts +12 -0
  26. package/dist/lib/i18n.d.ts.map +1 -1
  27. package/dist/lib/i18n.js +24 -0
  28. package/dist/lib/i18n.js.map +1 -1
  29. package/dist/lib/proxy-server.d.ts +22 -6
  30. package/dist/lib/proxy-server.d.ts.map +1 -1
  31. package/dist/lib/proxy-server.js +174 -75
  32. package/dist/lib/proxy-server.js.map +1 -1
  33. package/package.json +1 -1
  34. package/dist/commands/types.d.ts +0 -5
  35. package/dist/commands/types.d.ts.map +0 -1
  36. package/dist/commands/types.js +0 -82
  37. package/dist/commands/types.js.map +0 -1
  38. package/templates/storefront-minimal/.env.example +0 -10
  39. package/templates/storefront-minimal/.github/workflows/build-template.yml +0 -119
  40. package/templates/storefront-minimal/app/globals.css +0 -18
  41. package/templates/storefront-minimal/app/layout.tsx +0 -26
  42. package/templates/storefront-minimal/app/page.tsx +0 -93
  43. package/templates/storefront-minimal/lib/graphql-client.ts +0 -23
  44. package/templates/storefront-minimal/next.config.ts +0 -15
  45. package/templates/storefront-minimal/open-next.config.ts +0 -3
  46. package/templates/storefront-minimal/package.json +0 -30
  47. package/templates/storefront-minimal/postcss.config.mjs +0 -5
  48. package/templates/storefront-minimal/tailwind.config.ts +0 -14
  49. package/templates/storefront-minimal/tsconfig.json +0 -27
  50. package/templates/storefront-minimal/wrangler.toml +0 -24
  51. package/templates/storefront-nextjs/.env.example +0 -68
  52. package/templates/storefront-nextjs/.github/workflows/build-template.yml +0 -119
  53. package/templates/storefront-nextjs/README.md +0 -524
  54. package/templates/storefront-nextjs/app/account/orders/page.tsx +0 -216
  55. package/templates/storefront-nextjs/app/account/page.tsx +0 -167
  56. package/templates/storefront-nextjs/app/auth/login/page.tsx +0 -135
  57. package/templates/storefront-nextjs/app/auth/register/page.tsx +0 -212
  58. package/templates/storefront-nextjs/app/cart/page.tsx +0 -263
  59. package/templates/storefront-nextjs/app/categories/[slug]/page.tsx +0 -200
  60. package/templates/storefront-nextjs/app/categories/page.tsx +0 -58
  61. package/templates/storefront-nextjs/app/checkout/page.tsx +0 -351
  62. package/templates/storefront-nextjs/app/collections/[slug]/page.tsx +0 -158
  63. package/templates/storefront-nextjs/app/collections/page.tsx +0 -61
  64. package/templates/storefront-nextjs/app/globals.css +0 -98
  65. package/templates/storefront-nextjs/app/layout.tsx +0 -39
  66. package/templates/storefront-nextjs/app/page.tsx +0 -136
  67. package/templates/storefront-nextjs/app/products/[slug]/page.tsx +0 -119
  68. package/templates/storefront-nextjs/app/products/page.tsx +0 -107
  69. package/templates/storefront-nextjs/app/search/page.tsx +0 -127
  70. package/templates/storefront-nextjs/components/auth/auth-guard.tsx +0 -94
  71. package/templates/storefront-nextjs/components/commerce/add-to-cart-button.tsx +0 -77
  72. package/templates/storefront-nextjs/components/commerce/cart-icon.tsx +0 -29
  73. package/templates/storefront-nextjs/components/commerce/currency-selector.tsx +0 -217
  74. package/templates/storefront-nextjs/components/commerce/pagination.tsx +0 -62
  75. package/templates/storefront-nextjs/components/commerce/product-actions.tsx +0 -135
  76. package/templates/storefront-nextjs/components/commerce/product-filters.tsx +0 -109
  77. package/templates/storefront-nextjs/components/commerce/product-price.tsx +0 -375
  78. package/templates/storefront-nextjs/components/commerce/search-input.tsx +0 -178
  79. package/templates/storefront-nextjs/components/commerce/sort-select.tsx +0 -64
  80. package/templates/storefront-nextjs/components/commerce/variant-selector.tsx +0 -210
  81. package/templates/storefront-nextjs/components/layout/footer.tsx +0 -107
  82. package/templates/storefront-nextjs/components/layout/header.tsx +0 -104
  83. package/templates/storefront-nextjs/components/providers.tsx +0 -62
  84. package/templates/storefront-nextjs/lib/auth/routes.ts +0 -52
  85. package/templates/storefront-nextjs/lib/currency.tsx +0 -140
  86. package/templates/storefront-nextjs/lib/format.ts +0 -159
  87. package/templates/storefront-nextjs/lib/graphql-queries.ts +0 -629
  88. package/templates/storefront-nextjs/lib/hooks.ts +0 -30
  89. package/templates/storefront-nextjs/middleware.ts +0 -80
  90. package/templates/storefront-nextjs/next.config.ts +0 -37
  91. package/templates/storefront-nextjs/open-next.config.ts +0 -3
  92. package/templates/storefront-nextjs/package.dev.json +0 -30
  93. package/templates/storefront-nextjs/package.json +0 -32
  94. package/templates/storefront-nextjs/package.json.template +0 -32
  95. package/templates/storefront-nextjs/postcss.config.mjs +0 -8
  96. package/templates/storefront-nextjs/tailwind.config.ts +0 -111
  97. package/templates/storefront-nextjs/tsconfig.json +0 -27
  98. package/templates/storefront-nextjs/wrangler.toml +0 -24
@@ -1,119 +0,0 @@
1
- name: Build & Publish Template
2
-
3
- on:
4
- push:
5
- branches: [main, master]
6
- tags: ['v*']
7
- workflow_dispatch:
8
- inputs:
9
- version:
10
- description: 'Version to publish (e.g., 1.0.0)'
11
- required: false
12
-
13
- env:
14
- # Repository name (e.g., "default1", "minimal", "shadcn")
15
- # Worker will be named: storefront-template-{TEMPLATE_NAME}
16
- TEMPLATE_NAME: ${{ github.event.repository.name }}
17
-
18
- jobs:
19
- build:
20
- runs-on: ubuntu-latest
21
- steps:
22
- - uses: actions/checkout@v4
23
-
24
- - name: Normalize template name
25
- id: template
26
- run: |
27
- # Strip "storefront-" prefix if present
28
- RAW_NAME="${{ env.TEMPLATE_NAME }}"
29
- NORMALIZED="${RAW_NAME#storefront-}"
30
- echo "name=${NORMALIZED}" >> $GITHUB_OUTPUT
31
- echo "Template name: ${NORMALIZED}"
32
-
33
- - name: Setup Node.js
34
- uses: actions/setup-node@v4
35
- with:
36
- node-version: '20'
37
-
38
- - name: Setup pnpm
39
- uses: pnpm/action-setup@v4
40
- with:
41
- version: 9
42
-
43
- - name: Install dependencies
44
- run: pnpm install
45
-
46
- - name: Build for Cloudflare (OpenNext)
47
- run: npx @opennextjs/cloudflare build
48
-
49
- - name: List build output
50
- run: |
51
- echo "=== Build output ==="
52
- ls -la .open-next/ || echo "No .open-next directory"
53
- if [ -f ".open-next/worker.js" ]; then
54
- echo "Worker size: $(wc -c < .open-next/worker.js) bytes"
55
- fi
56
-
57
- - name: Create deployment wrangler.toml
58
- run: |
59
- cat > .open-next/wrangler.toml << EOF
60
- name = "storefront-template-${{ steps.template.outputs.name }}"
61
- main = "worker.js"
62
- compatibility_date = "2024-09-23"
63
- compatibility_flags = ["nodejs_compat"]
64
- assets = { directory = "assets", binding = "ASSETS" }
65
-
66
- [observability]
67
- enabled = true
68
- head_sampling_rate = 1
69
-
70
- [observability.logs]
71
- enabled = true
72
- head_sampling_rate = 1
73
- persist = true
74
- invocation_logs = true
75
- EOF
76
-
77
- - name: Determine version
78
- id: version
79
- run: |
80
- if [ -n "${{ github.event.inputs.version }}" ]; then
81
- echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
82
- elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then
83
- echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
84
- else
85
- echo "version=$(jq -r .version package.json)-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
86
- fi
87
-
88
- - name: Deploy to Cloudflare Workers
89
- working-directory: .open-next
90
- env:
91
- CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
92
- CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
93
- run: |
94
- if [ -z "$CLOUDFLARE_API_TOKEN" ]; then
95
- echo "⏭️ Skipping CF deploy (CLOUDFLARE_API_TOKEN not set)"
96
- exit 0
97
- fi
98
- npx wrangler@latest deploy
99
-
100
- - name: Upload worker bundle artifact
101
- uses: actions/upload-artifact@v4
102
- with:
103
- name: worker-bundle
104
- path: .open-next/
105
- retention-days: 7
106
- if-no-files-found: error
107
- include-hidden-files: true
108
-
109
- - name: Publish to DoSwiftly (on tag or manual)
110
- if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch'
111
- env:
112
- DOSWIFTLY_TOKEN: ${{ secrets.DOSWIFTLY_DEPLOY_TOKEN }}
113
- DOSWIFTLY_API_URL: ${{ secrets.DOSWIFTLY_API_URL }}
114
- run: |
115
- # Upload bundle to DoSwiftly R2 via API
116
- curl -X POST "${DOSWIFTLY_API_URL}/cli/templates/${{ steps.template.outputs.name }}/publish" \
117
- -H "Authorization: Bearer ${DOSWIFTLY_TOKEN}" \
118
- -F "bundle=@.open-next/worker.js" \
119
- -F "version=${{ steps.version.outputs.version }}"
@@ -1,524 +0,0 @@
1
- # DoSwiftly Storefront Template (DEPRECATED)
2
-
3
- > **DEPRECATED**: This template is no longer maintained. Use `storefront-nextjs-shadcn` instead, which follows the Hydrogen-style codegen-first architecture with local hook generation.
4
- >
5
- > This template imports pre-built hooks from `@doswiftly/storefront-sdk/graphql/client|server` which are no longer exported as of SDK v3.0.0.
6
-
7
- A modern, production-ready e-commerce storefront built with Next.js 15 and the DoSwiftly Commerce SDK.
8
-
9
- ## Features
10
-
11
- - ⚡ **Next.js 15** - App Router, Server Components, React 19
12
- - 🛒 **Commerce SDK** - Type-safe GraphQL API integration
13
- - 💱 **Multi-currency** - Automatic currency switching with SSR support
14
- - 🎨 **TailwindCSS** - Beautiful, responsive UI
15
- - 🔒 **Type-safe** - Full TypeScript support
16
- - 🚀 **Hybrid Architecture** - RSC for SEO, Client for interactivity
17
-
18
- ## Quick Start
19
-
20
- ```bash
21
- # Install dependencies
22
- pnpm install
23
-
24
- # Start development server
25
- pnpm dev
26
- ```
27
-
28
- ---
29
-
30
- ## Architecture: RSC vs Client Components
31
-
32
- This template uses a **hybrid architecture** that combines React Server Components (RSC) for performance and SEO with Client Components for interactivity.
33
-
34
- ### When to Use RSC (Server)
35
-
36
- | Use Case | Reason |
37
- | ------------------- | ----------------------- |
38
- | Product listings | SEO, cacheability |
39
- | Category pages | SEO, static content |
40
- | Collection pages | SEO, static content |
41
- | Search results | SEO for search engines |
42
- | Single product info | SEO, initial load speed |
43
-
44
- ### When to Use Client Components
45
-
46
- | Use Case | Reason |
47
- | ------------------ | ------------------------------ |
48
- | Shopping cart | Real-time updates, mutations |
49
- | Add to cart button | User interaction |
50
- | Account/profile | User-specific, mutations |
51
- | Checkout | Forms, payments, interactivity |
52
- | Currency selector | Client-side state |
53
-
54
- ### Route Architecture
55
-
56
- ```
57
- /app
58
- ├── layout.tsx (RSC)
59
- │ └── providers.tsx (Client) ← QueryClient + StorefrontProvider
60
-
61
- ├── page.tsx (RSC) ← useProducts, useCategories
62
- ├── products/
63
- │ ├── page.tsx (RSC) ← useProducts
64
- │ └── [slug]/page.tsx (RSC + Client island)
65
- │ └── <ProductActions/> (Client) ← Add to cart
66
-
67
- ├── categories/
68
- │ ├── page.tsx (RSC) ← useCategories
69
- │ └── [slug]/page.tsx (RSC) ← useCategory, useProducts
70
-
71
- ├── collections/
72
- │ ├── page.tsx (RSC) ← useCollections
73
- │ └── [slug]/page.tsx (RSC) ← useCollection, useProducts
74
-
75
- ├── search/page.tsx (RSC) ← useProducts
76
-
77
- ├── cart/page.tsx (Client) ← useCart, useCartLinesUpdate
78
-
79
- ├── account/
80
- │ ├── page.tsx (Client) ← useCustomer
81
- │ └── login/page.tsx (Client) ← useCustomerLogin
82
-
83
- └── checkout/page.tsx (Client) ← Full interactivity
84
- ```
85
-
86
- ---
87
-
88
- ## Data Fetching
89
-
90
- > **🔑 IMPORTANT:** Both server and client return **normalized data** (flat arrays).
91
- > GraphQL edges/nodes are automatically unwrapped. No `.edges.map((e) => e.node)` needed!
92
- >
93
- > This provides the best developer experience and seamless migration between RSC and Client components.
94
-
95
- ### Server Components (RSC)
96
-
97
- Import from `@doswiftly/storefront-sdk/graphql/server`:
98
-
99
- ```typescript
100
- import {
101
- useProducts,
102
- useProduct,
103
- useCategories,
104
- } from "@doswiftly/storefront-sdk/graphql/server";
105
-
106
- // All functions return normalized data (no edges/nodes)
107
- // Cached per-request via React cache()
108
- export default async function ProductsPage() {
109
- const { products, pageInfo } = await useProducts({ first: 10 });
110
- const { categories } = await useCategories();
111
-
112
- // products is already a flat array - no .edges.map needed!
113
- return <ProductGrid products={products} />;
114
- }
115
- ```
116
-
117
- **Available server helpers:**
118
-
119
- - `useProducts()` - Product listings with pagination
120
- - `useProduct()` - Single product by handle
121
- - `useCategories()` - All categories
122
- - `useCategory()` - Single category by slug
123
- - `useCollections()` - All collections
124
- - `useCollection()` - Single collection by handle
125
- - `useCart()` - Cart by ID
126
- - `useShop()` - Shop configuration
127
- - `useCustomer()` - Authenticated customer data
128
-
129
- ### Client Components
130
-
131
- Import from `@doswiftly/storefront-sdk/graphql/client`:
132
-
133
- ```tsx
134
- "use client";
135
-
136
- import {
137
- useProducts,
138
- useCart,
139
- useCartLinesAdd,
140
- } from "@doswiftly/storefront-sdk/graphql/client";
141
-
142
- export function ProductList() {
143
- // React Query hook - returns normalized data (same as server!)
144
- const { data, isLoading } = useProducts({ first: 10 });
145
-
146
- if (isLoading) return <Skeleton />;
147
-
148
- // data.products is already a flat array!
149
- return <Grid products={data?.products} />;
150
- }
151
-
152
- export function AddToCartButton({ variantId }: { variantId: string }) {
153
- const { mutate: addToCart, isPending } = useCartLinesAdd();
154
-
155
- return (
156
- <button
157
- onClick={() =>
158
- addToCart({
159
- cartId,
160
- lines: [{ merchandiseId: variantId, quantity: 1 }],
161
- })
162
- }
163
- disabled={isPending}
164
- >
165
- {isPending ? "Adding..." : "Add to Cart"}
166
- </button>
167
- );
168
- }
169
- ```
170
-
171
- **Available client hooks:**
172
-
173
- Queries:
174
-
175
- - `useProducts()` - Product listings
176
- - `useProduct()` - Single product
177
- - `useCategories()` - Categories
178
- - `useCategory()` - Single category
179
- - `useCollections()` - Collections
180
- - `useCollection()` - Single collection
181
- - `useCart()` - Cart data
182
- - `useCustomer()` - Customer data
183
-
184
- Mutations:
185
-
186
- - `useCartCreate()` - Create new cart
187
- - `useCartLinesAdd()` - Add items to cart
188
- - `useCartLinesUpdate()` - Update cart items
189
- - `useCartLinesRemove()` - Remove from cart
190
- - `useCustomerLogin()` - Login
191
- - `useCustomerCreate()` - Register
192
- - `useCustomerLogout()` - Logout
193
-
194
- ---
195
-
196
- ## Naming Convention
197
-
198
- Both server and client use **identical function names AND return structures**:
199
-
200
- ```typescript
201
- // Server (RSC) - async function, returns normalized data
202
- import { useProducts } from "@doswiftly/storefront-sdk/graphql/server";
203
- const { products, pageInfo } = await useProducts({ first: 10 });
204
- // products is a flat array ✅
205
-
206
- // Client - React Query hook, returns normalized data (same!)
207
- import { useProducts } from "@doswiftly/storefront-sdk/graphql/client";
208
- const { data } = useProducts({ first: 10 });
209
- const products = data?.products; // Also a flat array ✅
210
- ```
211
-
212
- ### Return Types Comparison
213
-
214
- | Aspect | Server (`/server`) | Client (`/client`) |
215
- | ----------- | ------------------------------------- | -------------------------------------------- |
216
- | Return type | `Promise<NormalizedProductsResponse>` | `UseQueryResult<NormalizedProductsResponse>` |
217
- | Data access | `const { products, pageInfo }` | `const { data }` → `data.products` |
218
- | Data format | **Flat array** (normalized, no edges) | **Flat array** (normalized, no edges) |
219
- | Loading | N/A (await) | `isLoading`, `isFetching` |
220
- | Error | try/catch | `error`, `isError` |
221
- | Refetch | Call again | `refetch()` |
222
- | Cache | `React.cache()` per-request | React Query client cache |
223
-
224
- ---
225
-
226
- ## Authentication & Session Persistence
227
-
228
- ### How It Works
229
-
230
- 1. **Login** → `useCustomerLogin()` mutation returns token
231
- 2. **Token stored** in cookie: `doswiftly_customer_token`
232
- 3. **Page refresh**:
233
- - RSC reads cookie → injects `X-Customer-Token` header
234
- - Client hydrates with same state from server props
235
- 4. **No flash of unauthenticated content**
236
-
237
- ### Implementation
238
-
239
- ```tsx
240
- // RSC - automatically authenticated via cookies
241
- import { useCustomer } from "@doswiftly/storefront-sdk/graphql/server";
242
-
243
- export default async function AccountPage() {
244
- const { customer } = await useCustomer();
245
-
246
- if (!customer) {
247
- redirect("/login");
248
- }
249
-
250
- return <Profile customer={customer} />;
251
- }
252
-
253
- // Client - same pattern with loading state
254
- ("use client");
255
- import { useCustomer, useAuth } from "@doswiftly/storefront-sdk/graphql/client";
256
-
257
- export function ProfileClient() {
258
- const { data, isLoading } = useCustomer();
259
- const { logout } = useAuth();
260
-
261
- if (isLoading) return <Skeleton />;
262
- if (!data?.customer) return <LoginPrompt />;
263
-
264
- return <Profile customer={data.customer} onLogout={logout} />;
265
- }
266
- ```
267
-
268
- ---
269
-
270
- ## Currency Handling
271
-
272
- ### Automatic Currency Injection
273
-
274
- Currency is read from `doswiftly_preferred_currency` cookie and automatically applied to all GraphQL requests.
275
-
276
- ### Changing Currency
277
-
278
- ```tsx
279
- "use client";
280
- import { useCurrency } from "@doswiftly/storefront-sdk/graphql/react";
281
- import { useRouter } from "next/navigation";
282
-
283
- export function CurrencySelector() {
284
- const { currency, setCurrency, availableCurrencies } = useCurrency();
285
- const router = useRouter();
286
-
287
- const handleChange = (newCurrency: string) => {
288
- setCurrency(newCurrency); // Updates cookie + context
289
- router.refresh(); // Refreshes RSC pages with new currency
290
- };
291
-
292
- return (
293
- <select value={currency} onChange={(e) => handleChange(e.target.value)}>
294
- {availableCurrencies.map((c) => (
295
- <option key={c} value={c}>
296
- {c}
297
- </option>
298
- ))}
299
- </select>
300
- );
301
- }
302
- ```
303
-
304
- ---
305
-
306
- ## Filtering & Sorting
307
-
308
- ### Filter Products
309
-
310
- ```typescript
311
- const { products } = await useProducts({
312
- first: 20,
313
- filters: {
314
- minPrice: 50,
315
- maxPrice: 200,
316
- vendor: "Nike",
317
- available: true,
318
- },
319
- sortKey: "PRICE",
320
- reverse: false, // ascending
321
- });
322
- ```
323
-
324
- ### Available Filters
325
-
326
- | Filter | Type | Description |
327
- | ------------- | ------- | ------------------ |
328
- | `minPrice` | number | Minimum price |
329
- | `maxPrice` | number | Maximum price |
330
- | `vendor` | string | Brand/vendor name |
331
- | `available` | boolean | Stock availability |
332
- | `productType` | string | Product type |
333
- | `tag` | string | Product tag |
334
-
335
- ### Sort Keys
336
-
337
- - `TITLE` - Alphabetically
338
- - `PRICE` - By price
339
- - `CREATED_AT` - By date
340
- - `BEST_SELLING` - By popularity
341
-
342
- ---
343
-
344
- ## Pagination
345
-
346
- Cursor-based pagination for all list endpoints:
347
-
348
- ```typescript
349
- const { products, pageInfo } = await useProducts({ first: 10 });
350
-
351
- if (pageInfo?.hasNextPage) {
352
- const nextPage = await useProducts({
353
- first: 10,
354
- after: pageInfo.endCursor,
355
- });
356
- }
357
- ```
358
-
359
- ---
360
-
361
- ## Island of Interactivity Pattern
362
-
363
- Combine RSC and Client Components for optimal performance:
364
-
365
- ```tsx
366
- // app/products/[slug]/page.tsx (RSC)
367
- import { useProduct } from "@doswiftly/storefront-sdk/graphql/server";
368
- import { AddToCartButton } from "@/components/commerce/add-to-cart";
369
-
370
- export default async function ProductPage({ params }) {
371
- const { product } = await useProduct({ handle: params.slug });
372
-
373
- return (
374
- <div>
375
- {/* RSC - Static, SEO-friendly */}
376
- <h1>{product.title}</h1>
377
- <p>{product.description}</p>
378
- <img src={product.featuredImage?.url} alt={product.title} />
379
-
380
- {/* Client Island - Interactive */}
381
- <AddToCartButton productId={product.id} variants={product.variants} />
382
- </div>
383
- );
384
- }
385
- ```
386
-
387
- ```tsx
388
- // components/commerce/add-to-cart.tsx (Client)
389
- "use client";
390
-
391
- import { useCartManager } from "@doswiftly/storefront-sdk/graphql/react";
392
-
393
- export function AddToCartButton({ productId, variants }) {
394
- const { addItem, loading } = useCartManager();
395
- const [quantity, setQuantity] = useState(1);
396
- const [selectedVariant, setSelectedVariant] = useState(variants[0]?.id);
397
-
398
- return (
399
- <div>
400
- <QuantitySelector value={quantity} onChange={setQuantity} />
401
- <VariantSelector
402
- variants={variants}
403
- value={selectedVariant}
404
- onChange={setSelectedVariant}
405
- />
406
- <button
407
- onClick={() => addItem(selectedVariant, quantity)}
408
- disabled={loading}
409
- >
410
- {loading ? "Adding..." : "Add to Cart"}
411
- </button>
412
- </div>
413
- );
414
- }
415
- ```
416
-
417
- ---
418
-
419
- ## Environment Variables
420
-
421
- ```env
422
- # Required
423
- NEXT_PUBLIC_API_URL=http://localhost:8000
424
- NEXT_PUBLIC_SHOP_SLUG=your-shop-slug
425
-
426
- # Optional
427
- NEXT_PUBLIC_SITE_NAME=My Store
428
- ```
429
-
430
- ---
431
-
432
- ## Project Structure
433
-
434
- ```
435
- app/
436
- layout.tsx # Root layout (RSC)
437
- page.tsx # Homepage (RSC)
438
- products/
439
- page.tsx # Product listing (RSC)
440
- [slug]/page.tsx # Product detail (RSC + Client)
441
- categories/
442
- page.tsx # Categories (RSC)
443
- [slug]/page.tsx # Category products (RSC)
444
- collections/
445
- page.tsx # Collections (RSC)
446
- [slug]/page.tsx # Collection products (RSC)
447
- search/page.tsx # Search results (RSC)
448
- cart/page.tsx # Shopping cart (Client)
449
- account/
450
- page.tsx # Account dashboard (Client)
451
- login/page.tsx # Login form (Client)
452
-
453
- components/
454
- commerce/
455
- add-to-cart.tsx # Add to cart button (Client)
456
- currency-selector.tsx # Currency switcher (Client)
457
- product-actions.tsx # Product page actions (Client)
458
- providers.tsx # QueryClient + StorefrontProvider (Client)
459
- ```
460
-
461
- ---
462
-
463
- ## Migration from Raw GraphQL
464
-
465
- If you're migrating from a previous version that returned raw GraphQL responses (edges/nodes), here's what changed:
466
-
467
- ### ✅ Before (Raw GraphQL)
468
-
469
- ```tsx
470
- // Client hooks returned raw edges/nodes
471
- const { data } = useProducts({ first: 10 });
472
- const products = data?.products.edges.map((e) => e.node);
473
- ```
474
-
475
- ### ✅ After (Normalized)
476
-
477
- ```tsx
478
- // Client hooks now return flat arrays (same as server)
479
- const { data } = useProducts({ first: 10 });
480
- const products = data?.products; // Already flat!
481
- ```
482
-
483
- ### Migration Steps
484
-
485
- 1. **Remove `.edges.map((e) => e.node)`** from all client components
486
- 2. **Access data directly**: `data.products` instead of `data.products.edges`
487
- 3. **PageInfo unchanged**: Still accessed via `data.pageInfo`
488
-
489
- ### Why This Change?
490
-
491
- - **Consistency**: Server and client APIs are now identical
492
- - **Developer Experience**: Zero boilerplate, less code
493
- - **Fewer Errors**: No more "Cannot read properties of undefined (reading 'map')"
494
- - **Seamless Migration**: Move components between RSC/Client without changes
495
-
496
- ### Need Raw GraphQL?
497
-
498
- For advanced use cases requiring raw GraphQL (custom fragments, \_\_typename, etc.), use `graphql-request` directly:
499
-
500
- ```typescript
501
- import { GraphQLClient } from "graphql-request";
502
- import { gql } from "graphql-tag";
503
-
504
- const client = new GraphQLClient(apiUrl);
505
- const data = await client.request(gql`
506
- query {
507
- products(first: 10) {
508
- edges {
509
- node {
510
- id
511
- __typename
512
- }
513
- cursor
514
- }
515
- }
516
- }
517
- `);
518
- ```
519
-
520
- ---
521
-
522
- ## License
523
-
524
- MIT