@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.
- package/dist/commands/check.js +2 -2
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +8 -5
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts +13 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +155 -63
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +3 -4
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +271 -166
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/sdk.d.ts +1 -1
- package/dist/commands/sdk.js +3 -3
- package/dist/commands/sdk.js.map +1 -1
- package/dist/commands/template.d.ts.map +1 -1
- package/dist/commands/template.js +4 -31
- package/dist/commands/template.js.map +1 -1
- package/dist/commands/verify.js +5 -5
- package/dist/commands/verify.js.map +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/i18n.d.ts +12 -0
- package/dist/lib/i18n.d.ts.map +1 -1
- package/dist/lib/i18n.js +24 -0
- package/dist/lib/i18n.js.map +1 -1
- package/dist/lib/proxy-server.d.ts +22 -6
- package/dist/lib/proxy-server.d.ts.map +1 -1
- package/dist/lib/proxy-server.js +174 -75
- package/dist/lib/proxy-server.js.map +1 -1
- package/package.json +1 -1
- package/dist/commands/types.d.ts +0 -5
- package/dist/commands/types.d.ts.map +0 -1
- package/dist/commands/types.js +0 -82
- package/dist/commands/types.js.map +0 -1
- package/templates/storefront-minimal/.env.example +0 -10
- package/templates/storefront-minimal/.github/workflows/build-template.yml +0 -119
- package/templates/storefront-minimal/app/globals.css +0 -18
- package/templates/storefront-minimal/app/layout.tsx +0 -26
- package/templates/storefront-minimal/app/page.tsx +0 -93
- package/templates/storefront-minimal/lib/graphql-client.ts +0 -23
- package/templates/storefront-minimal/next.config.ts +0 -15
- package/templates/storefront-minimal/open-next.config.ts +0 -3
- package/templates/storefront-minimal/package.json +0 -30
- package/templates/storefront-minimal/postcss.config.mjs +0 -5
- package/templates/storefront-minimal/tailwind.config.ts +0 -14
- package/templates/storefront-minimal/tsconfig.json +0 -27
- package/templates/storefront-minimal/wrangler.toml +0 -24
- package/templates/storefront-nextjs/.env.example +0 -68
- package/templates/storefront-nextjs/.github/workflows/build-template.yml +0 -119
- package/templates/storefront-nextjs/README.md +0 -524
- package/templates/storefront-nextjs/app/account/orders/page.tsx +0 -216
- package/templates/storefront-nextjs/app/account/page.tsx +0 -167
- package/templates/storefront-nextjs/app/auth/login/page.tsx +0 -135
- package/templates/storefront-nextjs/app/auth/register/page.tsx +0 -212
- package/templates/storefront-nextjs/app/cart/page.tsx +0 -263
- package/templates/storefront-nextjs/app/categories/[slug]/page.tsx +0 -200
- package/templates/storefront-nextjs/app/categories/page.tsx +0 -58
- package/templates/storefront-nextjs/app/checkout/page.tsx +0 -351
- package/templates/storefront-nextjs/app/collections/[slug]/page.tsx +0 -158
- package/templates/storefront-nextjs/app/collections/page.tsx +0 -61
- package/templates/storefront-nextjs/app/globals.css +0 -98
- package/templates/storefront-nextjs/app/layout.tsx +0 -39
- package/templates/storefront-nextjs/app/page.tsx +0 -136
- package/templates/storefront-nextjs/app/products/[slug]/page.tsx +0 -119
- package/templates/storefront-nextjs/app/products/page.tsx +0 -107
- package/templates/storefront-nextjs/app/search/page.tsx +0 -127
- package/templates/storefront-nextjs/components/auth/auth-guard.tsx +0 -94
- package/templates/storefront-nextjs/components/commerce/add-to-cart-button.tsx +0 -77
- package/templates/storefront-nextjs/components/commerce/cart-icon.tsx +0 -29
- package/templates/storefront-nextjs/components/commerce/currency-selector.tsx +0 -217
- package/templates/storefront-nextjs/components/commerce/pagination.tsx +0 -62
- package/templates/storefront-nextjs/components/commerce/product-actions.tsx +0 -135
- package/templates/storefront-nextjs/components/commerce/product-filters.tsx +0 -109
- package/templates/storefront-nextjs/components/commerce/product-price.tsx +0 -375
- package/templates/storefront-nextjs/components/commerce/search-input.tsx +0 -178
- package/templates/storefront-nextjs/components/commerce/sort-select.tsx +0 -64
- package/templates/storefront-nextjs/components/commerce/variant-selector.tsx +0 -210
- package/templates/storefront-nextjs/components/layout/footer.tsx +0 -107
- package/templates/storefront-nextjs/components/layout/header.tsx +0 -104
- package/templates/storefront-nextjs/components/providers.tsx +0 -62
- package/templates/storefront-nextjs/lib/auth/routes.ts +0 -52
- package/templates/storefront-nextjs/lib/currency.tsx +0 -140
- package/templates/storefront-nextjs/lib/format.ts +0 -159
- package/templates/storefront-nextjs/lib/graphql-queries.ts +0 -629
- package/templates/storefront-nextjs/lib/hooks.ts +0 -30
- package/templates/storefront-nextjs/middleware.ts +0 -80
- package/templates/storefront-nextjs/next.config.ts +0 -37
- package/templates/storefront-nextjs/open-next.config.ts +0 -3
- package/templates/storefront-nextjs/package.dev.json +0 -30
- package/templates/storefront-nextjs/package.json +0 -32
- package/templates/storefront-nextjs/package.json.template +0 -32
- package/templates/storefront-nextjs/postcss.config.mjs +0 -8
- package/templates/storefront-nextjs/tailwind.config.ts +0 -111
- package/templates/storefront-nextjs/tsconfig.json +0 -27
- 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
|