@forgecart/cli 2.202608110054.0 → 2.202608160103.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/dist/src/commands/init.d.ts +16 -0
- package/dist/src/commands/init.js +18 -7
- package/dist/src/commands/init.js.map +1 -1
- package/package.json +1 -1
- package/templates/storefront/README.md +145 -61
- package/templates/storefront/next.config.js +20 -0
- package/templates/storefront/package.json +10 -2
- package/templates/storefront/postcss.config.js +1 -2
- package/templates/storefront/src/app/%5F%5Ffc/track/route.ts +23 -11
- package/templates/storefront/src/app/__forge_beacon/route.ts +1 -2
- package/templates/storefront/src/app/api/%5F%5Fbackend/methods/route.ts +27 -0
- package/templates/storefront/src/app/cart/page.tsx +33 -6
- package/templates/storefront/src/app/checkout/page.tsx +40 -0
- package/templates/storefront/src/app/error.tsx +21 -0
- package/templates/storefront/src/app/global-error.tsx +23 -0
- package/templates/storefront/src/app/globals.css +105 -8
- package/templates/storefront/src/app/layout.tsx +45 -17
- package/templates/storefront/src/app/page.tsx +153 -43
- package/templates/storefront/src/app/ping/route.ts +1 -2
- package/templates/storefront/src/app/products/[slug]/not-found.tsx +3 -8
- package/templates/storefront/src/app/products/[slug]/page.tsx +69 -23
- package/templates/storefront/src/app/products/page.tsx +52 -9
- package/templates/storefront/src/components/CartView.tsx +244 -117
- package/templates/storefront/src/components/ForgeTracker.tsx +40 -26
- package/templates/storefront/src/components/Header.tsx +8 -10
- package/templates/storefront/src/components/ProductCard.tsx +25 -13
- package/templates/storefront/src/components/ProductPurchase.tsx +13 -18
- package/templates/storefront/src/components/checkout/AddressStep.tsx +288 -0
- package/templates/storefront/src/components/checkout/CheckoutFlow.tsx +543 -0
- package/templates/storefront/src/components/checkout/CheckoutGate.tsx +45 -0
- package/templates/storefront/src/components/checkout/PaymentElementForm.tsx +138 -0
- package/templates/storefront/src/components/checkout/PaymentFormEmbed.tsx +89 -0
- package/templates/storefront/src/components/checkout/RatesStep.tsx +113 -0
- package/templates/storefront/src/instrumentation.ts +34 -0
- package/templates/storefront/src/lib/action-result.ts +30 -0
- package/templates/storefront/src/lib/backend-actions.ts +20 -0
- package/templates/storefront/src/lib/backend-client.ts +47 -0
- package/templates/storefront/src/lib/cart-context.tsx +157 -22
- package/templates/storefront/src/lib/checkout-session.ts +185 -0
- package/templates/storefront/src/lib/error-messages.ts +24 -0
- package/templates/storefront/src/lib/experiments.ts +42 -44
- package/templates/storefront/src/lib/forgecart.ts +61 -78
- package/templates/storefront/src/lib/format.ts +91 -0
- package/templates/storefront/src/lib/session-actions.ts +54 -0
- package/templates/storefront/src/lib/shop-config.ts +44 -0
- package/templates/storefront/src/lib/shop-session.ts +114 -0
- package/templates/storefront/src/lib/uuid.ts +19 -0
- package/templates/storefront/src/server/app.module.ts +18 -0
- package/templates/storefront/src/server/backend-api.ts +26 -0
- package/templates/storefront/src/server/backend-method.decorator.ts +23 -0
- package/templates/storefront/src/server/bootstrap.ts +122 -0
- package/templates/storefront/src/server/customer-extras/customer-extras.module.ts +13 -0
- package/templates/storefront/src/server/customer-extras/service/customer-extras.service.ts +58 -0
- package/templates/storefront/src/server/customer-extras/type/customer-extras.types.ts +11 -0
- package/templates/storefront/src/server/forgecart/forgecart-client.factory.ts +69 -0
- package/templates/storefront/src/server/forgecart/forgecart.module.ts +9 -0
- package/templates/storefront/src/server/runner.ts +91 -0
- package/templates/storefront/src/server/types.ts +36 -0
- package/templates/storefront/tsconfig.json +2 -0
- package/templates/storefront/.env.example +0 -12
- package/templates/storefront/src/lib/cart-actions.ts +0 -139
- package/templates/storefront/tailwind.config.js +0 -8
|
@@ -22,8 +22,24 @@ export declare const DEPS_READY_MARKER_PATH = ".forgecart/deps-ready.json";
|
|
|
22
22
|
export interface InitOptions {
|
|
23
23
|
/** Channel token (required). */
|
|
24
24
|
token?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Channel admin secret for the storefront's server-side admin operations.
|
|
27
|
+
* Optional at the CLI boundary (a plain storefront needs only the shop
|
|
28
|
+
* token); the pod-provisioning path always passes it. Written to `.env`
|
|
29
|
+
* only — never `.forgecart/config.json`, which is committable metadata.
|
|
30
|
+
*/
|
|
31
|
+
adminSecret?: string;
|
|
25
32
|
/** Shop API base URL. */
|
|
26
33
|
apiUrl: string;
|
|
34
|
+
/**
|
|
35
|
+
* Shop API URL for the SHOPPER'S BROWSER (the client session socket), when
|
|
36
|
+
* it differs from `apiUrl`. Workspace pods have a split horizon: the pod's
|
|
37
|
+
* server side reaches the shop over an internal address (docker bridge /
|
|
38
|
+
* cluster network) that a shopper's browser can never use — and an HTTPS
|
|
39
|
+
* page must open its websocket against WSS. Absent = `apiUrl` serves both
|
|
40
|
+
* sides (deployed storefronts, loopback dev).
|
|
41
|
+
*/
|
|
42
|
+
browserApiUrl?: string;
|
|
27
43
|
/** Whether to also scaffold a NestJS application (planned for a later phase). */
|
|
28
44
|
application?: boolean;
|
|
29
45
|
/**
|
|
@@ -123,13 +123,22 @@ function buildConfig(channelToken, options) {
|
|
|
123
123
|
storefrontPath: '.',
|
|
124
124
|
};
|
|
125
125
|
}
|
|
126
|
-
/** Build the `.env` contents for the storefront SDK
|
|
127
|
-
function buildEnv(channelToken,
|
|
128
|
-
|
|
126
|
+
/** Build the `.env` contents for the storefront SDK clients. */
|
|
127
|
+
function buildEnv(channelToken, options) {
|
|
128
|
+
const lines = [
|
|
129
129
|
`FORGECART_CHANNEL_TOKEN=${channelToken}`,
|
|
130
|
-
`FORGECART_SHOP_API_URL=${apiUrl}`,
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
`FORGECART_SHOP_API_URL=${options.apiUrl}`,
|
|
131
|
+
];
|
|
132
|
+
const browserApiUrl = options.browserApiUrl?.trim();
|
|
133
|
+
if (browserApiUrl) {
|
|
134
|
+
lines.push(`FORGECART_SHOP_API_BROWSER_URL=${browserApiUrl}`);
|
|
135
|
+
}
|
|
136
|
+
const secret = options.adminSecret?.trim();
|
|
137
|
+
if (secret) {
|
|
138
|
+
lines.push(`FORGECART_ADMIN_SECRET=${secret}`);
|
|
139
|
+
}
|
|
140
|
+
lines.push('');
|
|
141
|
+
return lines.join('\n');
|
|
133
142
|
}
|
|
134
143
|
/**
|
|
135
144
|
* Write the per-channel config the storefront reads at runtime: `.forgecart/
|
|
@@ -142,7 +151,7 @@ async function writeChannelConfig(targetDir, channelToken, options) {
|
|
|
142
151
|
const configDir = join(targetDir, '.forgecart');
|
|
143
152
|
await mkdir(configDir, { recursive: true });
|
|
144
153
|
await writeFile(join(configDir, 'config.json'), `${JSON.stringify(buildConfig(channelToken, options), null, 2)}\n`, 'utf8');
|
|
145
|
-
await writeFile(join(targetDir, '.env'), buildEnv(channelToken, options
|
|
154
|
+
await writeFile(join(targetDir, '.env'), buildEnv(channelToken, options), 'utf8');
|
|
146
155
|
}
|
|
147
156
|
/** Print the post-scaffold next steps. */
|
|
148
157
|
function printNextSteps(targetDir, application, depsInstalled) {
|
|
@@ -223,7 +232,9 @@ export function createInitCommand() {
|
|
|
223
232
|
.description('Scaffold a ForgeCart storefront in the target directory')
|
|
224
233
|
.argument('[name]', 'Target directory name (defaults to the current directory)')
|
|
225
234
|
.option('-t, --token <token>', 'ForgeCart channel token (required)')
|
|
235
|
+
.option('--admin-secret <secret>', 'Channel admin secret for server-side admin operations (written to .env only)')
|
|
226
236
|
.option('--api-url <url>', 'Shop API base URL written to the storefront environment', DEFAULT_SHOP_API_URL)
|
|
237
|
+
.option('--browser-api-url <url>', 'Shop API URL for the shopper browser when it differs from --api-url (split-horizon pods; must be HTTPS so the session socket can use WSS)')
|
|
227
238
|
.option('-a, --application', 'Also scaffold a NestJS application (TODO: next phase)')
|
|
228
239
|
.option('--config-only', 'Write only per-channel config into an existing pre-warmed storefront (skip the template copy)')
|
|
229
240
|
.action(async (name, options) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/commands/init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAErC,0DAA0D;AAC1D,MAAM,CAAC,MAAM,oBAAoB,GAAG,2BAA2B,CAAC;AAEhE;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,wBAAwB,CAAC;AAEnE;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../../src/commands/init.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,87 +1,171 @@
|
|
|
1
1
|
# Storefront
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
A [Next.js](https://nextjs.org) (App Router) storefront for a ForgeCart
|
|
4
|
+
channel: product browsing, a server-backed cart, and a complete multi-step
|
|
5
|
+
checkout — all speaking the ForgeCart shop GraphQL API through the generated
|
|
6
|
+
`@forgecart/sdk` client, styled with the daisyUI design system.
|
|
6
7
|
|
|
7
8
|
This directory is the scaffold copied verbatim into your project by
|
|
8
9
|
`forgecart init`. Nothing here is compiled by the ForgeCart monorepo; it ships
|
|
9
10
|
as-is in the published `@forgecart/cli` package.
|
|
10
11
|
|
|
11
|
-
##
|
|
12
|
+
## Configuration
|
|
12
13
|
|
|
13
|
-
The storefront reads
|
|
14
|
+
The storefront reads three environment variables (server-side only):
|
|
14
15
|
|
|
15
16
|
| Variable | Purpose |
|
|
16
|
-
| ------------------------- |
|
|
17
|
-
| `FORGECART_CHANNEL_TOKEN` | Identifies the channel; sent as the `forgecart-token` header.
|
|
18
|
-
| `FORGECART_SHOP_API_URL` | Base URL of the channel's shop GraphQL API (`.../shop-api`).
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
| ------------------------- | -------------------------------------------------------------- |
|
|
18
|
+
| `FORGECART_CHANNEL_TOKEN` | Identifies the channel; sent as the `forgecart-token` header. |
|
|
19
|
+
| `FORGECART_SHOP_API_URL` | Base URL of the channel's shop GraphQL API (`.../shop-api`). |
|
|
20
|
+
| `FORGECART_ADMIN_SECRET` | Channel admin secret for server-side admin operations. |
|
|
21
|
+
|
|
22
|
+
`forgecart init` writes all of them to `.env` — it is the sole author of that
|
|
23
|
+
file, and `.env` is the only env file this project ever has (no `.env.example`,
|
|
24
|
+
no `.env.local`). None of the values ever reaches the
|
|
25
|
+
browser: every module that constructs an SDK client starts with
|
|
26
|
+
`import 'server-only'`, so a client component importing one is a **build
|
|
27
|
+
error**, and the workspace-pod image build additionally asserts no SDK code
|
|
28
|
+
lands in the client chunks.
|
|
29
|
+
|
|
30
|
+
## The backend gate (`sdk.backend`)
|
|
31
|
+
|
|
32
|
+
The storefront embeds a real NestJS backend under `src/server/` — booted as
|
|
33
|
+
an application context (no HTTP server; Next.js owns HTTP) in
|
|
34
|
+
`instrumentation.ts`, with normal `@Module`/`@Injectable` structure. A
|
|
35
|
+
service method decorated with `@BackendMethod()` (and declared in
|
|
36
|
+
`backend-api.ts`) becomes callable from any client component through the
|
|
37
|
+
typed proxy:
|
|
25
38
|
|
|
26
39
|
```ts
|
|
27
|
-
|
|
28
|
-
endpoint: process.env.FORGECART_SHOP_API_URL,
|
|
29
|
-
channelToken: process.env.FORGECART_CHANNEL_TOKEN,
|
|
30
|
-
});
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
Because the client is only imported from Server Components, the channel token
|
|
34
|
-
never reaches the browser.
|
|
40
|
+
import { backend } from '@/lib/backend-client';
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
npm install
|
|
40
|
-
npm run dev # next dev --turbopack -> http://localhost:3000
|
|
42
|
+
const result = await backend.assignCustomerHash({});
|
|
43
|
+
if (!result.ok) renderError(result.error);
|
|
41
44
|
```
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
One Server Action is the transport — no routes, no fetch calls, no
|
|
47
|
+
websockets. Methods receive a per-invocation session `{ admin, shop,
|
|
48
|
+
hasSession }`: `admin` is the channel-admin SDK gate (from
|
|
49
|
+
`FORGECART_ADMIN_SECRET`) and each method owns what that authority may be
|
|
50
|
+
used for; inputs are untrusted. The decorator is the allowlist, boot fails
|
|
51
|
+
loudly if the decorator scan and `backend-api.ts` drift, and in dev
|
|
52
|
+
`/api/__backend/methods` lists the live registry. The worked example
|
|
53
|
+
(`src/server/customer-extras/`) mints a random referral hash for the
|
|
54
|
+
signed-in customer and stores it as an ACF custom-field entry.
|
|
55
|
+
|
|
56
|
+
## Design system
|
|
57
|
+
|
|
58
|
+
Styling is Tailwind v4 + daisyUI 5, and every component uses **semantic**
|
|
59
|
+
classes only (`bg-base-100`, `btn-primary`, `badge-secondary`, `steps`,
|
|
60
|
+
`alert`, …). The single source of brand truth is the `@plugin "daisyui/theme"`
|
|
61
|
+
block in [`src/app/globals.css`](src/app/globals.css) — restyling the whole
|
|
62
|
+
storefront means editing those tokens and nothing else.
|
|
63
|
+
|
|
64
|
+
## Data flow
|
|
65
|
+
|
|
66
|
+
**Transport.** The generated SDK carries every operation over graphql-ws — a
|
|
67
|
+
per-request client is a per-request websocket connection. All SDK calls run on
|
|
68
|
+
the server; nothing SDK-shaped ships to the browser.
|
|
69
|
+
|
|
70
|
+
- [`src/lib/forgecart.ts`](src/lib/forgecart.ts) — the channel-scoped
|
|
71
|
+
singleton client for anonymous reads (products, selling plans) via typed
|
|
72
|
+
helpers (`getProducts`, `getProductBySlug`, `getFeaturedProducts`,
|
|
73
|
+
`getChannelSellingPlanGroups`, …), plus the SDK type re-exports the
|
|
74
|
+
components render. Pure display helpers (`formatPrice`,
|
|
75
|
+
`getStartingPrice`, plan labels) live in
|
|
76
|
+
[`src/lib/format.ts`](src/lib/format.ts), which client components import
|
|
77
|
+
freely — it carries no SDK runtime.
|
|
78
|
+
- [`src/lib/shop-action.ts`](src/lib/shop-action.ts) — the Server-Action
|
|
79
|
+
runner: builds a per-request client around the shopper's
|
|
80
|
+
`forgecart-session` cookie, runs one operation, persists any newly minted
|
|
81
|
+
session token, translates failure into the typed envelope, and
|
|
82
|
+
`dispose()`s the client so no action leaks its socket.
|
|
83
|
+
- [`src/lib/action-result.ts`](src/lib/action-result.ts) — `ActionResult<T>`:
|
|
84
|
+
Server Actions **never throw** across the RSC boundary (Next production
|
|
85
|
+
redacts thrown action errors). Failures arrive in-band as the SDK's
|
|
86
|
+
`ExtractedError` (`code`, server-localized `message`, `variables`), which
|
|
87
|
+
the UI renders inline.
|
|
88
|
+
|
|
89
|
+
**Cart.** [`src/lib/cart-actions.ts`](src/lib/cart-actions.ts) drives the
|
|
90
|
+
shop **order** API — the real cart, never browser storage. The interactive
|
|
91
|
+
state lives in [`src/lib/cart-context.tsx`](src/lib/cart-context.tsx): seeded
|
|
92
|
+
with the server-fetched order, updated from each action's returned order, and
|
|
93
|
+
carrying the typed `error` channel the cart UI renders. Coupons
|
|
94
|
+
(`applyCoupon`/`removeCoupon`) recalculate discounts and totals server-side;
|
|
95
|
+
setting a line's quantity to `0` removes it.
|
|
96
|
+
|
|
97
|
+
**Checkout.** [`src/lib/checkout-actions.ts`](src/lib/checkout-actions.ts) +
|
|
98
|
+
[`src/components/checkout/`](src/components/checkout/) implement the
|
|
99
|
+
reference flow on daisyUI `steps`:
|
|
100
|
+
|
|
101
|
+
1. **Contact** — guest email + name (`setCustomerForOrder`).
|
|
102
|
+
2. **Address** — type-ahead via the shop `addressAutocomplete` API (optional
|
|
103
|
+
capability; manual entry always works), country select populated from
|
|
104
|
+
`availableCountries` (never a hardcoded list), and the **required phone
|
|
105
|
+
number** the API enforces.
|
|
106
|
+
3. **Shipping** — `refreshShippingRateGroups` quotes carriers explicitly (the
|
|
107
|
+
read query never does), rates render grouped per shipment with per-group
|
|
108
|
+
carrier failures, and the customer picks — the ArrangingPayment
|
|
109
|
+
transition requires a selection in every group.
|
|
110
|
+
4. **Payment** — eligible providers → `createPaymentSession` →
|
|
111
|
+
`getSessionTemplate` renders the provider's hosted form in
|
|
112
|
+
[`PaymentFormEmbed`](src/components/checkout/PaymentFormEmbed.tsx), which
|
|
113
|
+
listens for the origin-pinned `iframe-ready` / `payment-success` /
|
|
114
|
+
`payment-error` handshake. No provider-specific code exists in the
|
|
115
|
+
template; coupon edits freeze once a session exists (the amount is
|
|
116
|
+
locked at session creation).
|
|
117
|
+
5. **Confirmation** — the order code and summary.
|
|
118
|
+
|
|
119
|
+
Errors render where they belong: field-level for coded field errors
|
|
120
|
+
(`SHIPPING_ADDRESS_PHONE_REQUIRED`, `SHIPPING_ADDRESS_COUNTRY_*`,
|
|
121
|
+
`COUPON_CODE_*`), a step-level `alert` otherwise, with
|
|
122
|
+
[`src/app/error.tsx`](src/app/error.tsx) and `global-error.tsx` as render
|
|
123
|
+
backstops.
|
|
124
|
+
|
|
125
|
+
## Sessions
|
|
126
|
+
|
|
127
|
+
The anonymous session is minted by the API on the first cart mutation and
|
|
128
|
+
persisted into the httpOnly `forgecart-session` cookie by the action runner.
|
|
129
|
+
The `/__fc/track` analytics relay may **establish** that identity for pure
|
|
130
|
+
browsers but never replaces an existing cookie, and the tracker defers its
|
|
131
|
+
first flush so a fast add-to-cart wins the first-touch race.
|
|
46
132
|
|
|
47
133
|
## Project layout
|
|
48
134
|
|
|
49
135
|
```
|
|
50
136
|
src/
|
|
51
137
|
lib/
|
|
52
|
-
forgecart.ts
|
|
53
|
-
|
|
138
|
+
forgecart.ts # server-only shop client + typed reads + SDK types
|
|
139
|
+
format.ts # pure display helpers (client-safe)
|
|
140
|
+
shop-action.ts # per-request action runner (envelope + dispose)
|
|
141
|
+
action-result.ts # ActionResult<T> + fallback error
|
|
142
|
+
cart-actions.ts # cart Server Actions (incl. coupons)
|
|
143
|
+
checkout-actions.ts # checkout Server Actions
|
|
144
|
+
cart-context.tsx # client cart state + typed error channel
|
|
54
145
|
components/
|
|
55
|
-
Header.tsx
|
|
56
|
-
ProductCard.tsx
|
|
57
|
-
|
|
146
|
+
Header.tsx # top nav with live cart count
|
|
147
|
+
ProductCard.tsx # product tile
|
|
148
|
+
ProductPurchase.tsx # variant picker + purchase options + add-to-cart
|
|
149
|
+
CartView.tsx # cart lines, coupons, totals, checkout CTA
|
|
150
|
+
checkout/
|
|
151
|
+
CheckoutFlow.tsx # the stepped flow orchestrator
|
|
152
|
+
AddressStep.tsx # autocomplete + countries + phone
|
|
153
|
+
RatesStep.tsx # grouped radio-card rate selection
|
|
154
|
+
PaymentFormEmbed.tsx # origin-checked hosted-form host
|
|
58
155
|
app/
|
|
59
|
-
layout.tsx
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
cart/page.tsx
|
|
156
|
+
layout.tsx # html shell + header/footer + CartProvider
|
|
157
|
+
error.tsx # route render backstop
|
|
158
|
+
global-error.tsx # root render backstop
|
|
159
|
+
page.tsx # home: featured products
|
|
160
|
+
products/ # grid + [slug] detail
|
|
161
|
+
cart/page.tsx # cart
|
|
162
|
+
checkout/page.tsx # the stepped checkout
|
|
65
163
|
```
|
|
66
164
|
|
|
67
|
-
##
|
|
165
|
+
## Development
|
|
68
166
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
- `getStartingPrice(product)` and `formatPrice(minorUnits, currency)`
|
|
75
|
-
|
|
76
|
-
The generated typed product operations (`shopClient.product.shopProducts` /
|
|
77
|
-
`.shopProduct`) only select `id` + `optionGroups`, so these helpers use the
|
|
78
|
-
SDK's supported raw-query escape hatch (`shopClient.query<T>(document, vars)`)
|
|
79
|
-
to also fetch `name`, `slug`, `description`, `featuredAsset`, and `variants`
|
|
80
|
-
(price in minor units). Every selected field exists on the shop schema's
|
|
81
|
-
`Product` type — extend the documents in `forgecart.ts` to fetch more.
|
|
82
|
-
|
|
83
|
-
## Cart
|
|
84
|
-
|
|
85
|
-
The starter cart (`src/lib/cart-context.tsx` + `app/cart/page.tsx`) keeps state
|
|
86
|
-
in the browser via `localStorage`. To take real orders, wire it to the
|
|
87
|
-
ForgeCart `cart` / `checkout` shop operations exposed by the SDK.
|
|
167
|
+
```bash
|
|
168
|
+
npm install
|
|
169
|
+
npm run dev # Turbopack dev server (what workspace pods run)
|
|
170
|
+
npm run build # production build (what deploys run; see Procfile)
|
|
171
|
+
```
|
|
@@ -4,6 +4,18 @@ const nextConfig = {
|
|
|
4
4
|
// storefront with `next start` without a full node_modules tree.
|
|
5
5
|
output: 'standalone',
|
|
6
6
|
reactStrictMode: true,
|
|
7
|
+
// The embedded NestJS backend (src/server/) must be required at runtime
|
|
8
|
+
// from node_modules, not bundled: Nest's core carries optional
|
|
9
|
+
// peer-dependency requires (microservices, platform adapters) that the
|
|
10
|
+
// bundler would otherwise chase into "module not found" errors.
|
|
11
|
+
// NOTE: externalizing Nest keeps it out of the NORMAL server bundle, but
|
|
12
|
+
// `src/instrumentation.ts` compiles under its own rules and statically
|
|
13
|
+
// resolves Nest's lazy `require('class-transformer')` /
|
|
14
|
+
// `require('class-validator')` (class-serializer + ValidationPipe). Those
|
|
15
|
+
// optional peers are therefore REAL dependencies in package.json — remove
|
|
16
|
+
// them and `next build` dies with Module-not-found on the Epinio staging
|
|
17
|
+
// path (CI run 31764049991, both attempts) while dev mode keeps working.
|
|
18
|
+
serverExternalPackages: ['@nestjs/core', '@nestjs/common'],
|
|
7
19
|
// The visual-editor preview runs THIS dev server inside a workspace pod,
|
|
8
20
|
// embedded cross-origin in the dashboard's editor iframe. Next 16 ENFORCES
|
|
9
21
|
// `allowedDevOrigins`: it 403s any cross-site request to an internal dev
|
|
@@ -16,6 +28,7 @@ const nextConfig = {
|
|
|
16
28
|
// prod preview host `<code>.preview.forgecart.dev` — it must be listed
|
|
17
29
|
// explicitly. Dev-only — `next start` (deployed storefronts) ignores it.
|
|
18
30
|
allowedDevOrigins: [
|
|
31
|
+
'*.127.0.0.1.nip.io', // LOCAL preview ingress (k3s Traefik :8081) — the host every local pod serves under; without it Next dev 403s the store's own /_next chunks locally (2026-08-13; the wm real-next-app fixture pins the same contract)
|
|
19
32
|
'*.vm.forgecart.com', // `pnpm vm` Cloudflare-tunnel dashboards
|
|
20
33
|
'*.dev.forgecart.dev', // dev dashboard + dev preview subdomains
|
|
21
34
|
'*.forgecart.dev', // single-label deployed preview hosts
|
|
@@ -24,6 +37,13 @@ const nextConfig = {
|
|
|
24
37
|
'dashboard.test.forgecart.com', // test dashboard origin that embeds the editor
|
|
25
38
|
'dashboard.forgecart.com', // prod dashboard origin that embeds the editor
|
|
26
39
|
],
|
|
40
|
+
// PROGRESSIVE PRE-RENDERING (Next 16: Cache Components). Every route builds
|
|
41
|
+
// a STATIC shell and streams its dynamic holes (product data, experiment
|
|
42
|
+
// branches, the shopper's cart) per request — uncached IO must sit inside a
|
|
43
|
+
// Suspense boundary, which is exactly how the pages are structured. `next
|
|
44
|
+
// dev` semantics are unchanged; the built/published storefront gets the
|
|
45
|
+
// prerendered shells.
|
|
46
|
+
cacheComponents: true,
|
|
27
47
|
experimental: {
|
|
28
48
|
// Persist Turbopack's dev compile artifacts to `.next` so a pod can serve
|
|
29
49
|
// a PRE-WARMED `.next` baked at build time instead of paying a cold first
|
|
@@ -10,18 +10,26 @@
|
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@forgecart/designer-runtime": "^1.202607161748.0",
|
|
12
12
|
"@forgecart/sdk": "^1.2.6",
|
|
13
|
+
"@nestjs/common": "^11.1.14",
|
|
14
|
+
"@nestjs/core": "^11.1.14",
|
|
15
|
+
"class-transformer": "^0.5.1",
|
|
16
|
+
"class-validator": "^0.14.2",
|
|
13
17
|
"next": "^16.1.0",
|
|
14
18
|
"react": "^19.0.0",
|
|
15
19
|
"react-dom": "^19.0.0",
|
|
20
|
+
"reflect-metadata": "^0.2.2",
|
|
21
|
+
"rxjs": "^7.8.2",
|
|
22
|
+
"server-only": "^0.0.1",
|
|
16
23
|
"uuidv7": "^1.1.0"
|
|
17
24
|
},
|
|
18
25
|
"devDependencies": {
|
|
26
|
+
"@tailwindcss/postcss": "^4.2.1",
|
|
19
27
|
"@types/node": "^22.10.0",
|
|
20
28
|
"@types/react": "^19.0.0",
|
|
21
29
|
"@types/react-dom": "^19.0.0",
|
|
22
|
-
"
|
|
30
|
+
"daisyui": "^5.5.19",
|
|
23
31
|
"postcss": "^8.4.49",
|
|
24
|
-
"tailwindcss": "^
|
|
32
|
+
"tailwindcss": "^4.2.1",
|
|
25
33
|
"typescript": "^5.7.0"
|
|
26
34
|
}
|
|
27
35
|
}
|
|
@@ -26,13 +26,23 @@ import {
|
|
|
26
26
|
* one contract. This route owns everything batch-shaped: parsing, per-item
|
|
27
27
|
* validation, the bot gate, session threading, and cookie persistence.
|
|
28
28
|
*
|
|
29
|
-
* Session capture
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
29
|
+
* Session capture: the shop API surfaces a freshly minted session in the
|
|
30
|
+
* response `extensions` (`forgecart-auth-token`) exactly once; the first
|
|
31
|
+
* touched item captures it and the remaining items of the batch ride it as
|
|
32
|
+
* Bearer (one identity per batch). The relay may ESTABLISH the shopper
|
|
33
|
+
* identity — persisting the mint into the httpOnly `forgecart-session` cookie
|
|
34
|
+
* (same attributes as the cart actions') when the request arrived cookie-less
|
|
35
|
+
* — but it never REPLACES an existing cookie. The asymmetry is deliberate:
|
|
36
|
+
* an incoming cookie that the upstream re-mints against is either a live
|
|
37
|
+
* session owned by the cart path (clobbering it would vanish a just-created
|
|
38
|
+
* cart — the first-touch double-mint race) or a stale one, whose replacement
|
|
39
|
+
* is the cart path's job (the SDK adopts re-mints and the cart actions
|
|
40
|
+
* persist them; the relay replacing it here would race those writers).
|
|
41
|
+
*
|
|
42
|
+
* Residual window: two writers that BOTH start cookie-less inside the same
|
|
43
|
+
* in-flight window remain last-writer-wins. With the tracker's deferred first
|
|
44
|
+
* flush (`ForgeTracker`'s establishment grace) that requires a click-to-
|
|
45
|
+
* response race under ~100ms — accepted.
|
|
36
46
|
*
|
|
37
47
|
* Contract: per-item isolation (one upstream failure never kills the batch),
|
|
38
48
|
* `accepted:false` is TERMINAL for that item (the tracker never retries), and
|
|
@@ -44,7 +54,6 @@ import {
|
|
|
44
54
|
* escape is Next's documented way to serve a literal-underscore URL segment.
|
|
45
55
|
* A folder literally named `__fc` would silently 404.
|
|
46
56
|
*/
|
|
47
|
-
export const dynamic = 'force-dynamic';
|
|
48
57
|
|
|
49
58
|
const SESSION_COOKIE = 'forgecart-session';
|
|
50
59
|
/** Mirrors `cart-actions.ts#persistSession` — the cookie is one shared identity. */
|
|
@@ -154,9 +163,12 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
|
|
|
154
163
|
}
|
|
155
164
|
|
|
156
165
|
const response = NextResponse.json({ results });
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
|
|
166
|
+
// Establish-only persist: write the cookie ONLY when the request arrived
|
|
167
|
+
// without one. A mint against an EXISTING cookie is never persisted here —
|
|
168
|
+
// replacing a live session would vanish the cart it holds (first-touch
|
|
169
|
+
// double-mint race), and replacing a stale one belongs to the cart path,
|
|
170
|
+
// whose SDK capture + persist owns re-mints (see the module docstring).
|
|
171
|
+
if (!incomingSession && sessionToken) {
|
|
160
172
|
response.cookies.set(SESSION_COOKIE, sessionToken, {
|
|
161
173
|
httpOnly: true,
|
|
162
174
|
sameSite: 'lax',
|
|
@@ -10,10 +10,9 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Gated entirely on `NODE_ENV === 'development'`: a deployed `next start` storefront
|
|
12
12
|
* answers 404 here and forwards nothing — the whole beacon path ships only in the
|
|
13
|
-
* in-pod dev-server.
|
|
13
|
+
* in-pod dev-server. Request access keeps it out of the static prerender (cacheComponents) so it
|
|
14
14
|
* behaves identically under `next dev` and a dev build.
|
|
15
15
|
*/
|
|
16
|
-
export const dynamic = 'force-dynamic';
|
|
17
16
|
|
|
18
17
|
const BEACON_PORT = process.env.FORGE_BEACON_PORT ?? '3002';
|
|
19
18
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
import { getBackend } from '../../../../server/bootstrap';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* DEV-ONLY registry listing — the agent's verification surface for backend
|
|
7
|
+
* features. After authoring a method, the agent probes this route and
|
|
8
|
+
* confirms the new name is listed; a boot failure (the api-map drift check)
|
|
9
|
+
* surfaces here as a 500 carrying the actionable message.
|
|
10
|
+
*
|
|
11
|
+
* Gated on `NODE_ENV === 'development'`: the literal check is inlined by
|
|
12
|
+
* the bundler, so a production build answers 404 and dead-code-eliminates
|
|
13
|
+
* the body. Method NAMES are the only thing exposed — never inputs,
|
|
14
|
+
* outputs, or configuration.
|
|
15
|
+
*/
|
|
16
|
+
export async function GET(): Promise<NextResponse> {
|
|
17
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
18
|
+
return NextResponse.json({ error: 'not found' }, { status: 404 });
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const { registry } = await getBackend();
|
|
22
|
+
return NextResponse.json({ methods: [...registry.keys()].sort() });
|
|
23
|
+
} catch (error) {
|
|
24
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
25
|
+
return NextResponse.json({ error: reason }, { status: 500 });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -1,13 +1,40 @@
|
|
|
1
|
+
import { connection } from 'next/server';
|
|
2
|
+
import { Suspense } from 'react';
|
|
3
|
+
|
|
1
4
|
import { CartView } from '../../components/CartView';
|
|
2
5
|
import { getChannelSellingPlanGroups } from '../../lib/forgecart';
|
|
3
6
|
|
|
4
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Cart route. The shopper's cart itself lives on their OWN websocket
|
|
9
|
+
* (`CartView` hydrates it client-side); the server's only read here is the
|
|
10
|
+
* PUBLIC channel subscription plans off the anonymous channel singleton —
|
|
11
|
+
* streamed as a hole so the shell stays static (cacheComponents).
|
|
12
|
+
*/
|
|
13
|
+
export default function CartPage() {
|
|
14
|
+
return (
|
|
15
|
+
<Suspense fallback={<CartRouteSkeleton />}>
|
|
16
|
+
<CartWithPlans />
|
|
17
|
+
</Suspense>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
5
20
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
// "Subscribe to your whole order" box
|
|
21
|
+
async function CartWithPlans() {
|
|
22
|
+
await connection();
|
|
23
|
+
// Channel-wide subscription plans are public channel data — the one
|
|
24
|
+
// server-fetched input; the "Subscribe to your whole order" box renders
|
|
25
|
+
// only when there are any.
|
|
10
26
|
const channelGroups = await getChannelSellingPlanGroups();
|
|
11
|
-
|
|
12
27
|
return <CartView channelGroups={channelGroups} />;
|
|
13
28
|
}
|
|
29
|
+
|
|
30
|
+
function CartRouteSkeleton() {
|
|
31
|
+
return (
|
|
32
|
+
<div className="space-y-8" aria-busy="true" aria-label="Loading your cart">
|
|
33
|
+
<h1 className="text-3xl font-bold tracking-tight">Your cart</h1>
|
|
34
|
+
<div className="grid items-start gap-8 lg:grid-cols-[minmax(0,1fr)_360px]">
|
|
35
|
+
<div className="skeleton h-64 w-full"></div>
|
|
36
|
+
<div className="skeleton h-80 w-full"></div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { connection } from 'next/server';
|
|
2
|
+
import { Suspense } from 'react';
|
|
3
|
+
|
|
4
|
+
import { CheckoutGate } from '../../components/checkout/CheckoutGate';
|
|
5
|
+
import { getAvailableCountries } from '../../lib/forgecart';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Checkout entry. The server's only job here is the PUBLIC prefetch — the
|
|
9
|
+
* channel's shipping countries off the anonymous channel singleton, streamed
|
|
10
|
+
* as a hole (cacheComponents). The session work (the order itself, every
|
|
11
|
+
* checkout mutation) lives on the shopper's websocket: `CheckoutGate`
|
|
12
|
+
* hydrates the live cart client-side and mounts the flow, or shows the
|
|
13
|
+
* honest empty state.
|
|
14
|
+
*/
|
|
15
|
+
export default function CheckoutPage() {
|
|
16
|
+
return (
|
|
17
|
+
<div className="mx-auto max-w-2xl space-y-6">
|
|
18
|
+
<h1 className="text-2xl font-bold tracking-tight">Checkout</h1>
|
|
19
|
+
<Suspense fallback={<CheckoutRouteSkeleton />}>
|
|
20
|
+
<CheckoutWithCountries />
|
|
21
|
+
</Suspense>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function CheckoutWithCountries() {
|
|
27
|
+
await connection();
|
|
28
|
+
const countries = await getAvailableCountries();
|
|
29
|
+
return <CheckoutGate countries={countries} />;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function CheckoutRouteSkeleton() {
|
|
33
|
+
return (
|
|
34
|
+
<div className="space-y-4" aria-busy="true" aria-label="Loading checkout">
|
|
35
|
+
<div className="skeleton h-10 w-full"></div>
|
|
36
|
+
<div className="skeleton h-40 w-full"></div>
|
|
37
|
+
<div className="skeleton h-10 w-40"></div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Route-level backstop boundary. The action layer never throws (typed
|
|
5
|
+
* `ActionResult` envelopes end at inline UI), so landing here means a RENDER
|
|
6
|
+
* failed — e.g. a Server Component read against an unreachable shop API.
|
|
7
|
+
* Recovery is a re-render via `reset()`.
|
|
8
|
+
*/
|
|
9
|
+
export default function RouteError({ reset }: { error: Error; reset: () => void }) {
|
|
10
|
+
return (
|
|
11
|
+
<div className="mx-auto max-w-xl space-y-4 py-16 text-center">
|
|
12
|
+
<h1 className="text-2xl font-bold tracking-tight text-base-content">Something went wrong</h1>
|
|
13
|
+
<p className="text-base-content/60">
|
|
14
|
+
The store could not load this page. Please try again in a moment.
|
|
15
|
+
</p>
|
|
16
|
+
<button type="button" className="btn btn-primary" onClick={() => reset()}>
|
|
17
|
+
Try again
|
|
18
|
+
</button>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|