@weaverse/next 0.1.0-alpha.8 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -73
- package/dist/{chunk-6PMZ77RA.mjs → chunk-LH6AAEIK.mjs} +115 -8
- package/dist/chunk-LH6AAEIK.mjs.map +1 -0
- package/dist/index.js +260 -30
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +167 -46
- package/dist/index.mjs.map +1 -1
- package/dist/server.js +321 -91
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +205 -28
- package/dist/server.mjs.map +1 -1
- package/dist/types/client.d.ts +16 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/hooks.d.ts +20 -0
- package/dist/types/hooks.d.ts.map +1 -0
- package/dist/types/index.d.ts +34 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/item.d.ts +60 -0
- package/dist/types/item.d.ts.map +1 -0
- package/dist/types/loader.d.ts +25 -0
- package/dist/types/loader.d.ts.map +1 -0
- package/dist/types/main.d.ts +11 -0
- package/dist/types/main.d.ts.map +1 -0
- package/dist/types/provider.d.ts +82 -0
- package/dist/types/provider.d.ts.map +1 -0
- package/dist/types/registry.d.ts +15 -0
- package/dist/types/registry.d.ts.map +1 -0
- package/dist/types/renderer.d.ts +21 -0
- package/dist/types/renderer.d.ts.map +1 -0
- package/dist/types/request-info.d.ts +19 -0
- package/dist/types/request-info.d.ts.map +1 -0
- package/dist/types/revalidate-item.d.ts +82 -0
- package/dist/types/revalidate-item.d.ts.map +1 -0
- package/dist/types/root-provider.d.ts +62 -0
- package/dist/types/root-provider.d.ts.map +1 -0
- package/dist/types/runtime.d.ts +113 -0
- package/dist/types/runtime.d.ts.map +1 -0
- package/dist/types/seo.d.ts +55 -0
- package/dist/types/seo.d.ts.map +1 -0
- package/dist/types/server/configs.d.ts +27 -0
- package/dist/types/server/configs.d.ts.map +1 -0
- package/dist/types/server/normalize-page-url.d.ts +19 -0
- package/dist/types/server/normalize-page-url.d.ts.map +1 -0
- package/dist/types/server/revalidate-handler.d.ts +55 -0
- package/dist/types/server/revalidate-handler.d.ts.map +1 -0
- package/dist/types/server/server-client.d.ts +21 -0
- package/dist/types/server/server-client.d.ts.map +1 -0
- package/dist/types/server.d.ts +19 -0
- package/dist/types/server.d.ts.map +1 -0
- package/dist/types/studio-bridge.d.ts +16 -0
- package/dist/types/studio-bridge.d.ts.map +1 -0
- package/dist/types/studio-connect.d.ts +27 -0
- package/dist/types/studio-connect.d.ts.map +1 -0
- package/dist/types/studio-router.d.ts +50 -0
- package/dist/types/studio-router.d.ts.map +1 -0
- package/dist/types/studio-script-src.d.ts +12 -0
- package/dist/types/studio-script-src.d.ts.map +1 -0
- package/dist/types/theme-settings-store.d.ts +18 -0
- package/dist/types/theme-settings-store.d.ts.map +1 -0
- package/dist/types/translation-context.d.ts +156 -0
- package/dist/types/translation-context.d.ts.map +1 -0
- package/dist/types/translation-store.d.ts +44 -0
- package/dist/types/translation-store.d.ts.map +1 -0
- package/dist/types/types.d.ts +665 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/use-weaverse-next-studio.d.ts +34 -0
- package/dist/types/use-weaverse-next-studio.d.ts.map +1 -0
- package/dist/types/utils.d.ts +12 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/package.json +12 -8
- package/src/client.ts +102 -0
- package/src/hooks.ts +50 -0
- package/src/index.ts +153 -0
- package/src/item.ts +167 -0
- package/src/loader.ts +135 -0
- package/src/main.tsx +45 -0
- package/src/provider.tsx +225 -0
- package/src/registry.ts +63 -0
- package/src/renderer.tsx +100 -0
- package/src/request-info.ts +124 -0
- package/src/revalidate-item.ts +211 -0
- package/src/root-provider.tsx +182 -0
- package/src/runtime.ts +555 -0
- package/src/seo.ts +157 -0
- package/src/server/configs.ts +178 -0
- package/src/server/normalize-page-url.ts +73 -0
- package/src/server/revalidate-handler.ts +357 -0
- package/src/server/server-client.ts +728 -0
- package/src/server.ts +67 -0
- package/src/studio-bridge.tsx +82 -0
- package/src/studio-connect.tsx +88 -0
- package/src/studio-router.ts +85 -0
- package/src/studio-script-src.ts +128 -0
- package/src/theme-settings-store.ts +47 -0
- package/src/translation-context.tsx +299 -0
- package/src/translation-store.ts +69 -0
- package/src/types.ts +753 -0
- package/src/use-weaverse-next-studio.tsx +106 -0
- package/src/utils.ts +37 -0
- package/dist/chunk-6PMZ77RA.mjs.map +0 -1
- package/dist/index.d.mts +0 -517
- package/dist/index.d.ts +0 -517
- package/dist/seo-CpN6tC36.d.mts +0 -477
- package/dist/seo-CpN6tC36.d.ts +0 -477
- package/dist/server.d.mts +0 -99
- package/dist/server.d.ts +0 -99
package/README.md
CHANGED
|
@@ -45,30 +45,7 @@ Implementation lives in [`src/server/server-client.ts`](./src/server/server-clie
|
|
|
45
45
|
- forces `no-store` for design/revision preview reads;
|
|
46
46
|
- returns safe client-facing configs including `requestInfo`.
|
|
47
47
|
|
|
48
|
-
### 3.
|
|
49
|
-
|
|
50
|
-
Yes. The POC started as a Next App Router app plus Shopify Hydrogen preview setup:
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
npx create-next-app@latest weaverse-hydrogen-next-poc --ts --app --eslint --tailwind --no-src-dir --import-alias "@/*" --use-npm --yes
|
|
54
|
-
npx @shopify/hydrogen@preview setup
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
See the POC findings doc:
|
|
58
|
-
|
|
59
|
-
- https://github.com/Weaverse/weaverse-hydrogen-next-poc/blob/main/findings.md
|
|
60
|
-
|
|
61
|
-
### 4. Where does the POC load page + Weaverse data?
|
|
62
|
-
|
|
63
|
-
Reference POC files:
|
|
64
|
-
|
|
65
|
-
- Server client helper: https://github.com/Weaverse/weaverse-hydrogen-next-poc/blob/main/app/weaverse-next/server.ts
|
|
66
|
-
- Home route load: https://github.com/Weaverse/weaverse-hydrogen-next-poc/blob/main/app/page.tsx
|
|
67
|
-
- Product route load: https://github.com/Weaverse/weaverse-hydrogen-next-poc/blob/main/app/products/%5Bhandle%5D/page.tsx
|
|
68
|
-
- Collection route load: https://github.com/Weaverse/weaverse-hydrogen-next-poc/blob/main/app/collections/%5Bhandle%5D/page.tsx
|
|
69
|
-
- Client wrapper/renderer: https://github.com/Weaverse/weaverse-hydrogen-next-poc/blob/main/app/weaverse-next/wrapper.tsx
|
|
70
|
-
- Studio script connector: https://github.com/Weaverse/weaverse-hydrogen-next-poc/blob/main/app/weaverse-next/studio-connect.tsx
|
|
71
|
-
- Per-item revalidation route: https://github.com/Weaverse/weaverse-hydrogen-next-poc/blob/main/app/api/weaverse/revalidate/route.ts
|
|
48
|
+
### 3. Where does a route load page + Weaverse data?
|
|
72
49
|
|
|
73
50
|
The route pattern is:
|
|
74
51
|
|
|
@@ -85,13 +62,13 @@ export default async function Home(props: {
|
|
|
85
62
|
}
|
|
86
63
|
```
|
|
87
64
|
|
|
88
|
-
###
|
|
65
|
+
### 4. Is there a global app load context like Hydrogen/Pilot?
|
|
89
66
|
|
|
90
67
|
Not automatically from the framework.
|
|
91
68
|
|
|
92
69
|
Hydrogen/React Router injects `context.weaverse` into route loaders. Next App Router does not have that loader-context mechanism. The Next equivalent is a small app-owned server helper that creates the request-scoped Weaverse server client at the route boundary.
|
|
93
70
|
|
|
94
|
-
|
|
71
|
+
Name it whatever you like — `getWeaverseServerClient(...)` in `app/weaverse-next/server.ts` below. It plays the same role as Hydrogen's global load context, but explicitly:
|
|
95
72
|
|
|
96
73
|
```ts
|
|
97
74
|
let weaverse = await getWeaverseServerClient(searchParams, pathname)
|
|
@@ -152,7 +129,9 @@ Main exports:
|
|
|
152
129
|
- `revalidateWeaverseNextItem()` — client-side per-item revalidation helper wired by `WeaverseNextStudio`.
|
|
153
130
|
- Hooks: `useThemeSettings`, `useWeaversePageData`, `useWeaverseRootData`, `useWeaverseCommerce`.
|
|
154
131
|
- Re-exported migration helpers from `@weaverse/react`: `useWeaverse`, `useParentInstance`, `useItemInstance`, `useChildInstances`.
|
|
155
|
-
- `createSchema
|
|
132
|
+
- Full `@weaverse/schema` surface, including `createSchema`, schema types, validators, and schema helper utilities.
|
|
133
|
+
- `generateDataFromSchema(schema)` — framework-neutral default-setting extraction utility shared with component/theme schema flows.
|
|
134
|
+
- Public `WeaverseNextThemeSchema` type for root/server theme settings config.
|
|
156
135
|
|
|
157
136
|
## Server API (`@weaverse/next/server`)
|
|
158
137
|
|
|
@@ -168,15 +147,26 @@ Main exports:
|
|
|
168
147
|
|
|
169
148
|
Example helper for a Next route/server component boundary:
|
|
170
149
|
|
|
150
|
+
This helper is app-level (not exported by `@weaverse/next`); it plays the role
|
|
151
|
+
of Hydrogen's global load context, but the app owns route identity explicitly.
|
|
152
|
+
The App Router boundary supplies `pathname`, `pageType`, and `handle`, so the
|
|
153
|
+
same identity reaches page loads and per-item revalidation loaders. It uses the
|
|
154
|
+
exported `PageType` and `WeaverseNextRequestContext` types. The module defines
|
|
155
|
+
both helpers used by the route-handler example below:
|
|
156
|
+
|
|
171
157
|
```ts
|
|
172
158
|
import { createWeaverseNextServerClient } from '@weaverse/next/server'
|
|
159
|
+
import type { PageType, WeaverseNextRequestContext } from '@weaverse/next'
|
|
173
160
|
import { headers } from 'next/headers'
|
|
174
161
|
import { serverComponents } from './server-components'
|
|
175
162
|
import { getStaticStorefrontClient } from './storefront'
|
|
176
163
|
|
|
177
164
|
export async function getWeaverseServerClient(
|
|
178
165
|
searchParamsPromise: Promise<Record<string, string | string[] | undefined>>,
|
|
179
|
-
pathname = '/'
|
|
166
|
+
pathname = '/',
|
|
167
|
+
// Route identity the App Router segment owns. Seeding it here means loaders
|
|
168
|
+
// resolve the same page type / handle during SSR and per-item revalidation.
|
|
169
|
+
identity: { pageType?: PageType; handle?: string } = {}
|
|
180
170
|
) {
|
|
181
171
|
let [headersList, rawSearchParams] = await Promise.all([
|
|
182
172
|
headers(),
|
|
@@ -200,6 +190,27 @@ export async function getWeaverseServerClient(
|
|
|
200
190
|
}`
|
|
201
191
|
)
|
|
202
192
|
|
|
193
|
+
let requestContext: WeaverseNextRequestContext = {
|
|
194
|
+
url,
|
|
195
|
+
headers: requestHeaders,
|
|
196
|
+
searchParams,
|
|
197
|
+
pathname,
|
|
198
|
+
pageType: identity.pageType,
|
|
199
|
+
handle: identity.handle,
|
|
200
|
+
i18n: { country: 'US', language: 'EN', locale: 'en-US' },
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return createWeaverseServerClientFromContext(requestContext)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Explicit-context path used by the revalidation callback. The context has
|
|
207
|
+
// already been validated by the SDK handler; project/host/env remain app-owned.
|
|
208
|
+
export function createWeaverseServerClientFromContext(
|
|
209
|
+
requestContext: WeaverseNextRequestContext
|
|
210
|
+
) {
|
|
211
|
+
// A localized app should select/cache this client from its own supported
|
|
212
|
+
// market allowlist using requestContext.i18n. This compact example uses the
|
|
213
|
+
// default market configured by the app helper.
|
|
203
214
|
let storefront = getStaticStorefrontClient()
|
|
204
215
|
|
|
205
216
|
return createWeaverseNextServerClient({
|
|
@@ -218,32 +229,42 @@ export async function getWeaverseServerClient(
|
|
|
218
229
|
},
|
|
219
230
|
}
|
|
220
231
|
: undefined,
|
|
221
|
-
requestContext
|
|
222
|
-
url,
|
|
223
|
-
headers: requestHeaders,
|
|
224
|
-
searchParams,
|
|
225
|
-
pathname,
|
|
226
|
-
i18n: { country: 'US', language: 'EN', locale: 'en-US' },
|
|
227
|
-
},
|
|
232
|
+
requestContext,
|
|
228
233
|
cache: { revalidate: 60, tags: ['weaverse'] },
|
|
229
234
|
})
|
|
230
235
|
}
|
|
231
236
|
```
|
|
232
237
|
|
|
233
|
-
Then each route
|
|
238
|
+
Then each route stays close to the Hydrogen mental model while supplying the
|
|
239
|
+
route identity the segment owns — an `INDEX` home route and a handle-bearing
|
|
240
|
+
`PRODUCT` route:
|
|
234
241
|
|
|
235
242
|
```ts
|
|
236
|
-
|
|
243
|
+
// app/page.tsx — the INDEX segment owns the page type.
|
|
244
|
+
let weaverse = await getWeaverseServerClient(props.searchParams, '/', {
|
|
245
|
+
pageType: 'INDEX',
|
|
246
|
+
})
|
|
237
247
|
let data = await weaverse.loadPage({ type: 'INDEX' })
|
|
238
248
|
let theme = await weaverse.loadThemeSettings()
|
|
239
249
|
```
|
|
240
250
|
|
|
251
|
+
```ts
|
|
252
|
+
// app/products/[handle]/page.tsx — the PRODUCT segment supplies the handle.
|
|
253
|
+
let { handle } = await props.params
|
|
254
|
+
let weaverse = await getWeaverseServerClient(
|
|
255
|
+
props.searchParams,
|
|
256
|
+
`/products/${handle}`,
|
|
257
|
+
{ pageType: 'PRODUCT', handle }
|
|
258
|
+
)
|
|
259
|
+
let data = await weaverse.loadPage({ type: 'PRODUCT', handle })
|
|
260
|
+
```
|
|
261
|
+
|
|
241
262
|
### Server client members
|
|
242
263
|
|
|
243
264
|
| Member | Description |
|
|
244
265
|
| --- | --- |
|
|
245
266
|
| `loadPage(input?)` | Fetches the page from `/api/public/project`, builds `WeaverseNextLoaderData`, runs component loaders, and returns page/project/config data. In section preview mode it can synthesize a single-section preview page. |
|
|
246
|
-
| `loadThemeSettings(options?)` | Fetches `/api/public/project_configs`, merges schema defaults under merchant settings, returns theme settings/static content/schema data for design mode. |
|
|
267
|
+
| `loadThemeSettings(options?)` | Fetches `/api/public/project_configs`, merges schema defaults under merchant settings, returns theme settings/static content/schema data for design mode. When the theme schema declares `i18n` and `requestContext.i18n` supplies both `language` and `country`, it also fetches locale static-text overrides from `/api/translation/static` in parallel and returns them as `merchantOverrides`. A failed or skipped overrides fetch never fails theme settings. |
|
|
247
268
|
| `fetchCustomPages(options?)` | Fetches published custom pages from `/api/public/v1/projects/:projectId/custom-pages` for sitemap generation, paginating automatically and returning partial results if a later page fails. |
|
|
248
269
|
| `fetchWithCache<T>(url, options?)` | Next-aware fetch helper. Uses `cache: 'no-store'` in design/revision preview and `next: { revalidate, tags }` in published mode. |
|
|
249
270
|
| `resolveProjectId()` / `projectId` | Resolves project id from Studio query, config function/string, or env. |
|
|
@@ -282,12 +303,13 @@ Use the server entry in `generateMetadata()` to map Weaverse `page.seo` into a N
|
|
|
282
303
|
|
|
283
304
|
```ts
|
|
284
305
|
// app/pages/[handle]/page.tsx
|
|
306
|
+
import type { Metadata } from 'next'
|
|
285
307
|
import { getWeaverseNextSeoMetadata } from '@weaverse/next/server'
|
|
286
308
|
|
|
287
309
|
export async function generateMetadata(props: {
|
|
288
310
|
params: Promise<{ handle: string }>
|
|
289
311
|
searchParams: Promise<Record<string, string | string[] | undefined>>
|
|
290
|
-
}) {
|
|
312
|
+
}): Promise<Metadata> {
|
|
291
313
|
let { handle } = await props.params
|
|
292
314
|
let weaverse = await getWeaverseServerClient(props.searchParams, `/pages/${handle}`)
|
|
293
315
|
let data = await weaverse.loadPage({ type: 'PAGE', handle })
|
|
@@ -295,7 +317,17 @@ export async function generateMetadata(props: {
|
|
|
295
317
|
}
|
|
296
318
|
```
|
|
297
319
|
|
|
298
|
-
The helper is pure and
|
|
320
|
+
The helper is pure and has no runtime dependency on `next`; it only imports the
|
|
321
|
+
`Metadata` type. Its return value is directly assignable to Next `Metadata`.
|
|
322
|
+
|
|
323
|
+
Two Builder-only SEO values are normalized so Next can always render the result:
|
|
324
|
+
|
|
325
|
+
- Open Graph type `product` falls back to `website` (Next throws on `product`).
|
|
326
|
+
The other Builder types — `website`, `article`, `profile`, `video.other` — are
|
|
327
|
+
passed through unchanged.
|
|
328
|
+
- Twitter card types `app` and `player` fall back to `summary`, because Builder
|
|
329
|
+
does not collect the extra descriptors those cards need (Next throws without
|
|
330
|
+
the `app` fields, and a `player` card without a player URL renders empty).
|
|
299
331
|
|
|
300
332
|
### Custom pages for sitemap generation
|
|
301
333
|
|
|
@@ -336,16 +368,18 @@ import {
|
|
|
336
368
|
WeaverseNextProvider,
|
|
337
369
|
WeaverseNextRenderer,
|
|
338
370
|
} from '@weaverse/next'
|
|
339
|
-
import type {
|
|
371
|
+
import type {
|
|
372
|
+
WeaverseNextLoaderData,
|
|
373
|
+
WeaverseNextRequestInfo,
|
|
374
|
+
} from '@weaverse/next'
|
|
340
375
|
import { components } from './components'
|
|
341
376
|
|
|
342
377
|
export function WeaversePage({ data }: { data: WeaverseNextLoaderData }) {
|
|
378
|
+
// The server client returns `requestInfo` in the exported shape, so restore
|
|
379
|
+
// the full route identity — including `pageType` and `handle` — into the
|
|
380
|
+
// client request context instead of a narrow inline subset.
|
|
343
381
|
let requestInfo = data.configs?.requestInfo as
|
|
344
|
-
|
|
|
345
|
-
i18n?: { country?: string; language?: string; locale?: string }
|
|
346
|
-
pathname?: string
|
|
347
|
-
search?: string
|
|
348
|
-
}
|
|
382
|
+
| WeaverseNextRequestInfo
|
|
349
383
|
| undefined
|
|
350
384
|
let projectId =
|
|
351
385
|
(data.configs?.projectId as string | undefined) ?? data.project?.id
|
|
@@ -361,6 +395,8 @@ export function WeaversePage({ data }: { data: WeaverseNextLoaderData }) {
|
|
|
361
395
|
isPreviewMode: Boolean(data.configs?.isPreviewMode),
|
|
362
396
|
isRevisionPreview: Boolean(data.configs?.isRevisionPreview),
|
|
363
397
|
i18n: requestInfo?.i18n,
|
|
398
|
+
pageType: requestInfo?.pageType,
|
|
399
|
+
handle: requestInfo?.handle,
|
|
364
400
|
},
|
|
365
401
|
})
|
|
366
402
|
|
|
@@ -479,13 +515,38 @@ The root script connector and page-level renderer are both required for full Stu
|
|
|
479
515
|
|
|
480
516
|
Resource-picker edits should refresh only the affected item's server loader data. The happy path should not call `router.refresh()` because refreshing the whole RSC tree can remount the page and reset scroll.
|
|
481
517
|
|
|
482
|
-
Mount the route handler in the consuming app
|
|
518
|
+
Mount the route handler in the consuming app. `getClient` receives a second
|
|
519
|
+
argument — the validated, same-origin route context reconstructed from the
|
|
520
|
+
request body — so the revalidated loader runs with the same pathname, locale,
|
|
521
|
+
page type, handle, and ordinary search params as the active storefront route.
|
|
522
|
+
The two server-client helpers below are app-level consumer helpers, not exports
|
|
523
|
+
from `@weaverse/next`:
|
|
483
524
|
|
|
484
525
|
```ts
|
|
485
526
|
// app/api/weaverse/revalidate/route.ts
|
|
486
527
|
import { createWeaverseNextRevalidateHandler } from '@weaverse/next/server'
|
|
487
|
-
import {
|
|
528
|
+
import {
|
|
529
|
+
createWeaverseServerClientFromContext,
|
|
530
|
+
getWeaverseServerClient,
|
|
531
|
+
} from '../../../weaverse-next/server'
|
|
532
|
+
|
|
533
|
+
export const { POST } = createWeaverseNextRevalidateHandler({
|
|
534
|
+
// `requestContext` is validated browser input for route identity only.
|
|
535
|
+
// Project ID, Studio host, API base, and env must still come from
|
|
536
|
+
// server config — never from this context — and it is `undefined` for
|
|
537
|
+
// legacy request bodies that carry no route context.
|
|
538
|
+
getClient: (_request, requestContext) =>
|
|
539
|
+
requestContext
|
|
540
|
+
? createWeaverseServerClientFromContext(requestContext)
|
|
541
|
+
: getWeaverseServerClient(Promise.resolve({})),
|
|
542
|
+
})
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
The existing one-argument callback still works. A handler mounted with a
|
|
546
|
+
`(request) => client` callback ignores the optional second argument and keeps
|
|
547
|
+
compiling and running, so old wiring needs no change:
|
|
488
548
|
|
|
549
|
+
```ts
|
|
489
550
|
export const { POST } = createWeaverseNextRevalidateHandler({
|
|
490
551
|
getClient: () => getWeaverseServerClient(Promise.resolve({})),
|
|
491
552
|
})
|
|
@@ -495,15 +556,46 @@ Flow:
|
|
|
495
556
|
|
|
496
557
|
```text
|
|
497
558
|
Studio resource picker edit
|
|
498
|
-
-> Builder calls runtime.internal.revalidateItem(draftItem)
|
|
499
|
-
-> SDK
|
|
559
|
+
-> Builder calls runtime.internal.revalidateItem(draftItem) (unchanged)
|
|
560
|
+
-> SDK snapshots runtime.requestInfo, strips server-owned/transient controls
|
|
561
|
+
-> SDK POSTs { draftItem, routeContext? } to /api/weaverse/revalidate
|
|
562
|
+
-> handler validates + sanitizes routeContext at the trust boundary
|
|
563
|
+
-> handler reconstructs a same-origin WeaverseNextRequestContext
|
|
564
|
+
-> getClient(request, requestContext) builds the route-aware server client
|
|
500
565
|
-> route handler finds the registered component by draftItem.type
|
|
501
|
-
-> handler runs that component's loader with draft
|
|
502
|
-
-> response returns { loaderData }
|
|
566
|
+
-> handler runs that component's loader with draft data + client.requestContext
|
|
567
|
+
-> response returns { loaderData } with Cache-Control: no-store
|
|
503
568
|
-> SDK applies loaderData to the live item instance in place
|
|
504
569
|
```
|
|
505
570
|
|
|
506
|
-
If the route is missing or returns an error, Builder falls back to the older
|
|
571
|
+
If the route is missing or returns an error, Builder falls back to the older
|
|
572
|
+
route-refresh path.
|
|
573
|
+
|
|
574
|
+
### Trust boundary
|
|
575
|
+
|
|
576
|
+
The revalidation endpoint is public and every JSON field is attacker-controlled.
|
|
577
|
+
The SDK sanitizes `routeContext` on the client for clean traffic, but the route
|
|
578
|
+
handler is the security boundary and re-validates independently:
|
|
579
|
+
|
|
580
|
+
- Only `pathname`, sanitized `search`, a narrow i18n subset, a
|
|
581
|
+
`PageTypeSchema`-valid `pageType`, and a bounded `handle` cross the boundary.
|
|
582
|
+
Headers, cookies, auth, env, project ID, Studio host, API base, commerce
|
|
583
|
+
clients, the runtime, and the client are never serialized.
|
|
584
|
+
- Server-owned controls (`weaverseProjectId`, `weaverseHost`, `weaverseApiBase`,
|
|
585
|
+
`weaversePublicApiBase`, `weaverseVersion`, `projectId`) and transient transport
|
|
586
|
+
controls (`weaverseDraftItem`, `__weaverseDraftItem`, `_rsc`) are stripped
|
|
587
|
+
case-insensitively on both sides, so a crafted body cannot influence server
|
|
588
|
+
config resolution.
|
|
589
|
+
- The origin is fixed from the endpoint request before assigning any
|
|
590
|
+
browser-provided pathname/search, so input cannot change protocol, host, port,
|
|
591
|
+
or credentials. `pathname` and `url.pathname` share one URL-canonicalized value.
|
|
592
|
+
- A malformed present `routeContext` returns `400 { error: 'invalid-route-context' }`
|
|
593
|
+
before `getClient` runs; a missing `routeContext` is valid legacy input and
|
|
594
|
+
passes `undefined` to the callback.
|
|
595
|
+
- Route context is routing input, never an authorization decision. A registered
|
|
596
|
+
loader must not treat pathname, handle, page type, locale, or search as proof
|
|
597
|
+
of access; customer/private data still requires independent server-side session
|
|
598
|
+
and authorization checks derived from the actual request.
|
|
507
599
|
|
|
508
600
|
## Config resolution
|
|
509
601
|
|
|
@@ -514,33 +606,23 @@ Server config resolution intentionally mirrors Hydrogen where possible:
|
|
|
514
606
|
- API base: trusted request host → `WEAVERSE_PUBLIC_API_BASE` → non-production `WEAVERSE_HOST` → `https://api.weaverse.io`.
|
|
515
607
|
- public env: `PUBLIC_STORE_DOMAIN`, `PUBLIC_STOREFRONT_API_TOKEN`.
|
|
516
608
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
## POC reference
|
|
609
|
+
## Alpha migration notes
|
|
520
610
|
|
|
521
|
-
|
|
611
|
+
### After `0.1.0-alpha.9`
|
|
522
612
|
|
|
523
|
-
-
|
|
613
|
+
- The root `@weaverse/next` entry re-exports the full `@weaverse/schema` surface for Hydrogen migration parity. This includes `createSchema`, schema types, validators, and helper utilities.
|
|
614
|
+
- Theme schema fields are now structurally typed with `WeaverseNextThemeSchema` instead of `unknown` across client/root/server theme settings APIs. Most existing schema objects remain assignable, but alpha consumers with very loose local schema types may need to annotate/cast them before the next publish.
|
|
615
|
+
- `generateDataFromSchema` is public from the root entry and accepts component schemas or `WeaverseNextThemeSchema`.
|
|
616
|
+
- Version bumping still happens in the release flow; do not publish another alpha without bumping `packages/next/package.json` first.
|
|
524
617
|
|
|
525
|
-
|
|
618
|
+
### Bundle note
|
|
526
619
|
|
|
527
|
-
-
|
|
528
|
-
|
|
529
|
-
Important POC paths:
|
|
530
|
-
|
|
531
|
-
```text
|
|
532
|
-
app/weaverse-next/server.ts # explicit Next server context helper
|
|
533
|
-
app/weaverse-next/wrapper.tsx # client provider + renderer
|
|
534
|
-
app/weaverse-next/studio-connect.tsx # root Studio script connector
|
|
535
|
-
app/api/weaverse/revalidate/route.ts # per-item loader revalidation route
|
|
536
|
-
app/page.tsx # home page Weaverse load
|
|
537
|
-
app/products/[handle]/page.tsx # product route Weaverse load
|
|
538
|
-
app/collections/[handle]/page.tsx # collection route Weaverse load
|
|
539
|
-
```
|
|
620
|
+
The Next package build keeps `@weaverse/schema` as an external package re-export. It does not inline Zod/schema validators into `@weaverse/next`'s generated `dist/index.*` bundles. App-level bundle size still depends on what a consuming Next app imports and how its bundler tree-shakes `@weaverse/schema`.
|
|
540
621
|
|
|
541
622
|
## Current limitations
|
|
542
623
|
|
|
543
624
|
- This package is still alpha; API names may change before stable.
|
|
544
625
|
- Next does not provide Hydrogen's global React Router loader context, so apps should keep a small `getWeaverseServerClient(...)` helper.
|
|
545
|
-
-
|
|
626
|
+
- Apps must still wire their App Router route context explicitly for page loads and per-item revalidation; validate product/collection/custom/locale routes in the starter before treating them as production-ready.
|
|
627
|
+
- Global sections are Builder-owned; `@weaverse/next` should only need fixes if manual Studio QA finds a rendering/runtime boundary issue.
|
|
546
628
|
- Full Shopify request-handler/redirect parity is app-level follow-up work, not owned by this package yet.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { PageTypeSchema } from '@weaverse/schema';
|
|
2
|
+
|
|
1
3
|
// src/utils.ts
|
|
2
4
|
function generateDataFromSchema(schema) {
|
|
3
5
|
let data = {};
|
|
@@ -107,13 +109,22 @@ function getSearchParamsFromContext(context) {
|
|
|
107
109
|
}
|
|
108
110
|
return new URLSearchParams();
|
|
109
111
|
}
|
|
112
|
+
function withPathPrefix(pathname, i18n) {
|
|
113
|
+
let raw = typeof i18n?.pathPrefix === "string" ? i18n.pathPrefix : "";
|
|
114
|
+
let segments = raw.split("/").filter(Boolean);
|
|
115
|
+
let prefix = segments.length > 0 ? `/${segments.join("/")}` : "";
|
|
116
|
+
if (prefix === "" || pathname === prefix || pathname.startsWith(`${prefix}/`)) {
|
|
117
|
+
return pathname;
|
|
118
|
+
}
|
|
119
|
+
return pathname === "/" ? prefix : `${prefix}${pathname}`;
|
|
120
|
+
}
|
|
110
121
|
function getPathnameFromContext(context) {
|
|
111
122
|
if (context?.pathname) {
|
|
112
|
-
return context.pathname;
|
|
123
|
+
return withPathPrefix(context.pathname, context.i18n);
|
|
113
124
|
}
|
|
114
125
|
if (context?.url) {
|
|
115
126
|
let url = toUrl(context.url);
|
|
116
|
-
return url.pathname || "/";
|
|
127
|
+
return withPathPrefix(url.pathname || "/", context.i18n);
|
|
117
128
|
}
|
|
118
129
|
return "/";
|
|
119
130
|
}
|
|
@@ -154,9 +165,96 @@ function buildWeaverseNextRequestInfo(context) {
|
|
|
154
165
|
pathname: getPathnameFromContext(context),
|
|
155
166
|
search: search ? `?${search}` : "",
|
|
156
167
|
queries: toQueryRecord(searchParams),
|
|
157
|
-
...context?.i18n ? { i18n: context.i18n } : {}
|
|
168
|
+
...context?.i18n ? { i18n: context.i18n } : {},
|
|
169
|
+
...context?.pageType ? { pageType: context.pageType } : {},
|
|
170
|
+
...context?.handle ? { handle: context.handle } : {}
|
|
158
171
|
};
|
|
159
172
|
}
|
|
173
|
+
var DEFAULT_REVALIDATE_ENDPOINT = "/api/weaverse/revalidate";
|
|
174
|
+
var MAX_ROUTE_CONTEXT_HANDLE_LENGTH = 512;
|
|
175
|
+
var DENIED_ROUTE_CONTEXT_PARAMS = [
|
|
176
|
+
"weaverseprojectid",
|
|
177
|
+
"weaversehost",
|
|
178
|
+
"weaverseapikey",
|
|
179
|
+
"weaverseapibase",
|
|
180
|
+
"weaversepublicapibase",
|
|
181
|
+
"weaverseversion",
|
|
182
|
+
"projectid",
|
|
183
|
+
"weaversedraftitem",
|
|
184
|
+
"__weaversedraftitem",
|
|
185
|
+
"_rsc"
|
|
186
|
+
];
|
|
187
|
+
var ROUTE_CONTEXT_I18N_KEYS = [
|
|
188
|
+
"country",
|
|
189
|
+
"label",
|
|
190
|
+
"language",
|
|
191
|
+
"locale",
|
|
192
|
+
"pathPrefix"
|
|
193
|
+
];
|
|
194
|
+
function sanitizeRouteContextSearch(search) {
|
|
195
|
+
let params = new URLSearchParams(search);
|
|
196
|
+
let denied = new Set(DENIED_ROUTE_CONTEXT_PARAMS);
|
|
197
|
+
for (let key of [...new Set(params.keys())]) {
|
|
198
|
+
if (denied.has(key.toLowerCase())) {
|
|
199
|
+
params.delete(key);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
let serialized = params.toString();
|
|
203
|
+
return serialized ? `?${serialized}` : "";
|
|
204
|
+
}
|
|
205
|
+
function buildWeaverseNextRevalidateRouteContext(requestInfo) {
|
|
206
|
+
if (!requestInfo || typeof requestInfo.pathname !== "string" || !requestInfo.pathname) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
let routeContext = {
|
|
210
|
+
pathname: requestInfo.pathname,
|
|
211
|
+
search: sanitizeRouteContextSearch(requestInfo.search ?? "")
|
|
212
|
+
};
|
|
213
|
+
let { i18n } = requestInfo;
|
|
214
|
+
if (i18n && typeof i18n === "object") {
|
|
215
|
+
let picked = {};
|
|
216
|
+
for (let key of ROUTE_CONTEXT_I18N_KEYS) {
|
|
217
|
+
let value = i18n[key];
|
|
218
|
+
if (typeof value === "string") {
|
|
219
|
+
picked[key] = value;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (Object.keys(picked).length) {
|
|
223
|
+
routeContext.i18n = picked;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (requestInfo.pageType && PageTypeSchema.safeParse(requestInfo.pageType).success) {
|
|
227
|
+
routeContext.pageType = requestInfo.pageType;
|
|
228
|
+
}
|
|
229
|
+
if (typeof requestInfo.handle === "string" && requestInfo.handle && requestInfo.handle.length <= MAX_ROUTE_CONTEXT_HANDLE_LENGTH) {
|
|
230
|
+
routeContext.handle = requestInfo.handle;
|
|
231
|
+
}
|
|
232
|
+
return routeContext;
|
|
233
|
+
}
|
|
234
|
+
async function revalidateWeaverseNextItem(runtime, draftItem, endpoint = DEFAULT_REVALIDATE_ENDPOINT) {
|
|
235
|
+
let instance = runtime.itemInstances.get(draftItem.id);
|
|
236
|
+
if (!instance) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`[weaverse-next] No live item instance for "${draftItem.id}".`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
let routeContext = buildWeaverseNextRevalidateRouteContext(
|
|
242
|
+
runtime.requestInfo
|
|
243
|
+
);
|
|
244
|
+
let body = routeContext ? { draftItem, routeContext } : { draftItem };
|
|
245
|
+
let response = await fetch(endpoint, {
|
|
246
|
+
method: "POST",
|
|
247
|
+
headers: { "Content-Type": "application/json" },
|
|
248
|
+
body: JSON.stringify(body)
|
|
249
|
+
});
|
|
250
|
+
if (!response.ok) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
`[weaverse-next] Revalidate endpoint responded ${response.status}.`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
let { loaderData } = await response.json();
|
|
256
|
+
instance.setData({ loaderData: loaderData ?? null });
|
|
257
|
+
}
|
|
160
258
|
|
|
161
259
|
// src/seo.ts
|
|
162
260
|
function hasOpenGraphContent(seo) {
|
|
@@ -165,6 +263,15 @@ function hasOpenGraphContent(seo) {
|
|
|
165
263
|
function hasTwitterContent(seo) {
|
|
166
264
|
return Boolean(seo?.title || seo?.description || seo?.image || seo?.cardType);
|
|
167
265
|
}
|
|
266
|
+
function normalizeTwitterCard(cardType) {
|
|
267
|
+
return cardType === "summary_large_image" ? "summary_large_image" : "summary";
|
|
268
|
+
}
|
|
269
|
+
function normalizeOpenGraphType(type) {
|
|
270
|
+
if (type === "article" || type === "profile" || type === "video.other") {
|
|
271
|
+
return type;
|
|
272
|
+
}
|
|
273
|
+
return "website";
|
|
274
|
+
}
|
|
168
275
|
function formatWeaverseNextSeoMetadata(seo) {
|
|
169
276
|
let index = seo?.robots?.index !== false;
|
|
170
277
|
let follow = seo?.robots?.follow !== false;
|
|
@@ -192,13 +299,13 @@ function formatWeaverseNextSeoMetadata(seo) {
|
|
|
192
299
|
title: openGraph?.title,
|
|
193
300
|
description: openGraph?.description,
|
|
194
301
|
images: openGraph?.image ? [openGraph.image] : void 0,
|
|
195
|
-
type: openGraph?.type
|
|
302
|
+
type: normalizeOpenGraphType(openGraph?.type)
|
|
196
303
|
};
|
|
197
304
|
}
|
|
198
305
|
let twitter = seo.twitter;
|
|
199
306
|
if (hasTwitterContent(twitter)) {
|
|
200
307
|
metadata.twitter = {
|
|
201
|
-
card: twitter?.cardType
|
|
308
|
+
card: normalizeTwitterCard(twitter?.cardType),
|
|
202
309
|
title: twitter?.title,
|
|
203
310
|
description: twitter?.description,
|
|
204
311
|
images: twitter?.image ? [twitter.image] : void 0
|
|
@@ -214,6 +321,6 @@ function getWeaverseNextSeoMetadata(data) {
|
|
|
214
321
|
return formatWeaverseNextSeoMetadata(page?.seo ?? null);
|
|
215
322
|
}
|
|
216
323
|
|
|
217
|
-
export { buildWeaverseNextRequestInfo, formatWeaverseNextSeoMetadata, generateDataFromSchema, getWeaverseNextSeoMetadata, runWeaverseComponentLoaders };
|
|
218
|
-
//# sourceMappingURL=chunk-
|
|
219
|
-
//# sourceMappingURL=chunk-
|
|
324
|
+
export { DEFAULT_REVALIDATE_ENDPOINT, ROUTE_CONTEXT_I18N_KEYS, buildWeaverseNextRequestInfo, buildWeaverseNextRevalidateRouteContext, formatWeaverseNextSeoMetadata, generateDataFromSchema, getWeaverseNextSeoMetadata, revalidateWeaverseNextItem, runWeaverseComponentLoaders, sanitizeRouteContextSearch, withPathPrefix };
|
|
325
|
+
//# sourceMappingURL=chunk-LH6AAEIK.mjs.map
|
|
326
|
+
//# sourceMappingURL=chunk-LH6AAEIK.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils.ts","../src/loader.ts","../src/request-info.ts","../src/revalidate-item.ts","../src/seo.ts"],"names":[],"mappings":"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","file":"chunk-LH6AAEIK.mjs","sourcesContent":["import type { SchemaType } from '@weaverse/schema'\nimport type { WeaverseNextThemeSchema } from './types'\n\n/**\n * Generate default component data from a schema's `settings` inputs.\n *\n * Mirrors Hydrogen's `generateDataFromSchema` (settings → `{ [name]:\n * defaultValue }`) but is kept local so `@weaverse/next` doesn't depend on\n * `@weaverse/hydrogen`. Falls back to the deprecated `inspector` array when a\n * schema predates `settings`.\n */\nexport function generateDataFromSchema(\n schema: SchemaType | WeaverseNextThemeSchema | undefined\n): Record<string, unknown> {\n let data: Record<string, unknown> = {}\n if (!schema) {\n return data\n }\n\n let groups =\n (schema.settings as {\n inputs?: { name?: string; defaultValue?: unknown }[]\n }[]) ??\n (schema as { inspector?: typeof groups }).inspector ??\n []\n\n for (let group of groups) {\n for (let input of group?.inputs ?? []) {\n let { name, defaultValue } = input\n if (name && defaultValue !== null && defaultValue !== undefined) {\n data[name] = defaultValue\n }\n }\n }\n\n return data\n}\n","import type {\n WeaverseNextClient,\n WeaverseNextCommerceContext,\n WeaverseNextComponent,\n WeaverseNextComponentData,\n WeaverseNextLoaderData,\n WeaverseNextRequestContext,\n} from './types'\nimport { generateDataFromSchema } from './utils'\n\n/** Inputs for running registered component loaders over a page tree. */\nexport interface RunComponentLoadersArgs {\n /** Weaverse client containing the component registry. */\n client: WeaverseNextClient\n /** Request-scoped commerce override; defaults to `client.commerce`. */\n commerce?: WeaverseNextCommerceContext\n /** Request-context override; defaults to `client.requestContext`. */\n context?: WeaverseNextRequestContext\n /** Page payload to walk; defaults to `client.data`. */\n data?: WeaverseNextLoaderData | null\n}\n\nfunction isItemNode(node: unknown): node is WeaverseNextComponentData {\n return (\n typeof node === 'object' &&\n node !== null &&\n typeof (node as WeaverseNextComponentData).type === 'string'\n )\n}\n\nfunction cloneItem(item: WeaverseNextComponentData): WeaverseNextComponentData {\n return {\n ...item,\n children: Array.isArray(item.children)\n ? item.children.map((child) =>\n isItemNode(child) ? cloneItem(child) : { ...child }\n )\n : item.children,\n }\n}\n\n/**\n * Walk a serialized page tree and run each registered component's `loader`,\n * attaching the result to the item as `loaderData`.\n *\n * Loaders receive explicit `commerce.storefront` (and `weaverse.storefront` as\n * a compatibility alias via the client). Children are walked recursively when\n * they are inline item objects; flat `items` arrays are covered by the\n * top-level walk.\n *\n * @returns a cloned `data` object with `loaderData` attached to cloned items.\n */\nexport async function runWeaverseComponentLoaders(\n args: RunComponentLoadersArgs\n): Promise<WeaverseNextLoaderData | null> {\n let { client, context, commerce } = args\n let sourceData = args.data ?? client.data\n if (!sourceData) {\n return null\n }\n if (!sourceData.page.items.length) {\n return sourceData\n }\n\n let data: WeaverseNextLoaderData = {\n ...sourceData,\n page: {\n ...sourceData.page,\n items: sourceData.page.items.map(cloneItem),\n },\n }\n let items = data.page.items\n\n let componentMap = new Map<string, WeaverseNextComponent>()\n for (let component of client.components) {\n if (component?.schema?.type) {\n componentMap.set(component.schema.type, component)\n }\n }\n\n let visited = new Set<string>()\n let resolvedContext = context ?? client.requestContext\n let resolvedCommerce = commerce ?? client.commerce\n\n // Per-call `weaverse` alias. When a request-scoped `commerce` is passed, the\n // `weaverse.storefront` compatibility alias must resolve to *that* storefront,\n // not the client's construction-time commerce. Pilot-style loaders call\n // `weaverse.storefront.query(...)`, so a stale alias would query the wrong\n // (or an undefined) storefront. When no override is given we reuse the client\n // as-is to preserve its prototype getters.\n let resolvedWeaverse: WeaverseNextClient =\n resolvedCommerce === client.commerce\n ? client\n : {\n ...client,\n commerce: resolvedCommerce,\n storefront: resolvedCommerce?.storefront,\n }\n\n async function walk(nodes: WeaverseNextComponentData[]) {\n for (let item of nodes) {\n if (!item || visited.has(item.id)) {\n continue\n }\n visited.add(item.id)\n\n let component = componentMap.get(item.type)\n if (component?.loader) {\n try {\n item.loaderData = await component.loader({\n data: { ...generateDataFromSchema(component.schema), ...item.data },\n weaverse: resolvedWeaverse,\n context: resolvedContext,\n commerce: resolvedCommerce,\n })\n } catch (error) {\n console.warn('❌ Item loader run failed.', item.type, item.id, error)\n }\n }\n\n let children = item.children\n if (Array.isArray(children)) {\n let inlineChildren = children.filter(\n isItemNode\n ) as WeaverseNextComponentData[]\n if (inlineChildren.length) {\n await walk(inlineChildren)\n }\n }\n }\n }\n\n await walk(items)\n return data\n}\n","import type {\n WeaverseNextRequestContext,\n WeaverseNextRequestInfo,\n} from './types'\n\nfunction toUrl(value: string | URL): URL {\n return typeof value === 'string' ? new URL(value, 'http://localhost') : value\n}\n\nfunction getSearchParamsFromContext(\n context?: WeaverseNextRequestContext\n): URLSearchParams {\n if (context?.searchParams) {\n return new URLSearchParams(context.searchParams)\n }\n\n if (context?.url) {\n let url = toUrl(context.url)\n return new URLSearchParams(url.searchParams)\n }\n\n return new URLSearchParams()\n}\n\n/**\n * Put the request's market prefix (`i18n.pathPrefix`, e.g. `/de-de`) on a\n * pathname that does not carry it yet.\n *\n * A Next app builds its request context from the route path, and with\n * locale-segmented routes that path is usually unprefixed. Studio's address\n * bar follows `requestInfo.pathname`, so without the prefix it snaps back to\n * the default market after every navigation. A path that already carries the\n * prefix, and the default market's empty prefix, pass through unchanged.\n */\nexport function withPathPrefix(\n pathname: string,\n i18n?: WeaverseNextRequestContext['i18n']\n): string {\n let raw = typeof i18n?.pathPrefix === 'string' ? i18n.pathPrefix : ''\n // Split rather than regex-trim: `pathPrefix` is caller input, and an anchored\n // `/\\/+$/` backtracks polynomially on long runs of slashes.\n let segments = raw.split('/').filter(Boolean)\n let prefix = segments.length > 0 ? `/${segments.join('/')}` : ''\n if (\n prefix === '' ||\n pathname === prefix ||\n pathname.startsWith(`${prefix}/`)\n ) {\n return pathname\n }\n return pathname === '/' ? prefix : `${prefix}${pathname}`\n}\n\nfunction getPathnameFromContext(context?: WeaverseNextRequestContext): string {\n if (context?.pathname) {\n return withPathPrefix(context.pathname, context.i18n)\n }\n\n if (context?.url) {\n let url = toUrl(context.url)\n return withPathPrefix(url.pathname || '/', context.i18n)\n }\n\n return '/'\n}\n\nfunction coerceQueryValue(value: string): string | boolean {\n if (value === 'true') {\n return true\n }\n if (value === 'false') {\n return false\n }\n return value\n}\n\nfunction toQueryRecord(\n searchParams: URLSearchParams\n): Record<string, string | boolean> {\n let queries: Record<string, string | boolean> = {}\n for (let [key, value] of searchParams.entries()) {\n queries[key] = coerceQueryValue(value)\n }\n return queries\n}\n\nconst TRANSIENT_STUDIO_PARAMS = new Set([\n 'weaverseDraftItem',\n // Legacy name from the original design-mode draft-item spike. Keep it out of\n // runtime requestInfo too, in case an older Builder bundle is in front of a\n // newer Next storefront.\n '__weaverseDraftItem',\n])\n\nfunction getRequestInfoSearchParams(\n context?: WeaverseNextRequestContext\n): URLSearchParams {\n let searchParams = new URLSearchParams(getSearchParamsFromContext(context))\n for (let key of TRANSIENT_STUDIO_PARAMS) {\n searchParams.delete(key)\n }\n return searchParams\n}\n\n/**\n * Build the Hydrogen-like request info shape consumed by the Studio bridge.\n * Duplicate query keys keep the last value, matching `URLSearchParams` iteration\n * assignment and Hydrogen's practical query-object behavior.\n */\nexport function buildWeaverseNextRequestInfo(\n context?: WeaverseNextRequestContext\n): WeaverseNextRequestInfo {\n let searchParams = getRequestInfoSearchParams(context)\n let search = searchParams.toString()\n\n return {\n pathname: getPathnameFromContext(context),\n search: search ? `?${search}` : '',\n queries: toQueryRecord(searchParams),\n ...(context?.i18n ? { i18n: context.i18n } : {}),\n ...(context?.pageType ? { pageType: context.pageType } : {}),\n ...(context?.handle ? { handle: context.handle } : {}),\n }\n}\n","import type { PageType } from '@weaverse/schema'\nimport { PageTypeSchema } from '@weaverse/schema'\nimport type {\n WeaverseNextComponentData,\n WeaverseNextI18n,\n WeaverseNextRequestInfo,\n} from './types'\n\n/** Default App Router endpoint used for Studio item-loader revalidation. */\nexport const DEFAULT_REVALIDATE_ENDPOINT = '/api/weaverse/revalidate'\n\nconst MAX_ROUTE_CONTEXT_HANDLE_LENGTH = 512\n\n/**\n * Server-owned and transient controls stripped from the revalidation route\n * context on both sides of the trust boundary. Compared case-insensitively, so\n * every entry is stored lowercased. Client sanitization keeps normal traffic\n * clean; the route handler repeats this deny-list as the security boundary.\n *\n * - Server-owned: a crafted `?weaverseProjectId=`/`?weaverseHost=` must never\n * influence server config resolution (see `getWeaverseNextConfigs`), and\n * credential-shaped params never ride along in route context.\n * - Transient: draft-item and RSC transport params never describe route identity.\n */\nexport const DENIED_ROUTE_CONTEXT_PARAMS = [\n 'weaverseprojectid',\n 'weaversehost',\n 'weaverseapikey',\n 'weaverseapibase',\n 'weaversepublicapibase',\n 'weaverseversion',\n 'projectid',\n 'weaversedraftitem',\n '__weaversedraftitem',\n '_rsc',\n]\n\n/** i18n fields allowed to cross the browser boundary. */\nexport const ROUTE_CONTEXT_I18N_KEYS = [\n 'country',\n 'label',\n 'language',\n 'locale',\n 'pathPrefix',\n] as const\n\n/**\n * JSON-safe route subset that may cross the browser boundary alongside the\n * draft item. Never carries headers, cookies, auth, env, project ID, Studio\n * host, API keys/bases, commerce clients, the request context, runtime, or\n * client. `search` is canonical (preserves duplicate keys); the derived\n * `queries` record is intentionally omitted.\n */\nexport interface WeaverseNextRevalidateRouteContext {\n /** Route handle used for page assignment. */\n handle?: string\n /** Public locale and market identity allowed across the browser boundary. */\n i18n?: Pick<\n WeaverseNextI18n,\n 'country' | 'label' | 'language' | 'locale' | 'pathPrefix'\n >\n /** Weaverse page type resolved by the active route. */\n pageType?: PageType\n /** Canonical same-origin route pathname. */\n pathname: string\n /** Sanitized serialized query string, including the leading `?` when set. */\n search: string\n}\n\n/**\n * Minimal runtime surface the in-place apply needs. Structural (not the\n * concrete `WeaverseNextRuntime` class) so unit tests can drive it with a\n * plain mock. `requestInfo` stays optional: a real `WeaverseNextRuntime`\n * always supplies it, but when absent the client omits `routeContext` and the\n * legacy `{ draftItem }` request is preserved.\n */\nexport interface RevalidateItemRuntimeLike {\n /** Live item stores keyed by Weaverse item ID. */\n itemInstances: Map<\n string,\n {\n /** Merge refreshed loader data into the live item store. */\n setData: (update: Record<string, unknown>) => unknown\n }\n >\n /** Active route identity used to construct the optional revalidation payload. */\n requestInfo?: WeaverseNextRequestInfo\n}\n\n/**\n * Strip denied (server-owned/transient) controls from a `search` string and\n * re-serialize it. Preserves ordinary application queries, duplicate keys,\n * encoded Unicode, and mode controls (`isDesignMode`, `isPreviewMode`,\n * `__revisionId`, `sectionType`). Returns `''` or a `?`-prefixed string.\n */\nexport function sanitizeRouteContextSearch(search: string): string {\n let params = new URLSearchParams(search)\n let denied = new Set(DENIED_ROUTE_CONTEXT_PARAMS)\n for (let key of [...new Set(params.keys())]) {\n if (denied.has(key.toLowerCase())) {\n params.delete(key)\n }\n }\n let serialized = params.toString()\n return serialized ? `?${serialized}` : ''\n}\n\n/**\n * Build the sanitized {@link WeaverseNextRevalidateRouteContext} from the\n * active runtime's request info. Returns `undefined` without a usable pathname\n * so the caller omits `routeContext` and keeps the legacy request. This is not\n * the security boundary — the route handler re-validates and re-sanitizes.\n */\nexport function buildWeaverseNextRevalidateRouteContext(\n requestInfo?: WeaverseNextRequestInfo\n): WeaverseNextRevalidateRouteContext | undefined {\n if (\n !requestInfo ||\n typeof requestInfo.pathname !== 'string' ||\n !requestInfo.pathname\n ) {\n return\n }\n\n let routeContext: WeaverseNextRevalidateRouteContext = {\n pathname: requestInfo.pathname,\n search: sanitizeRouteContextSearch(requestInfo.search ?? ''),\n }\n\n let { i18n } = requestInfo\n if (i18n && typeof i18n === 'object') {\n let picked: Record<string, string> = {}\n for (let key of ROUTE_CONTEXT_I18N_KEYS) {\n let value = i18n[key]\n if (typeof value === 'string') {\n picked[key] = value\n }\n }\n if (Object.keys(picked).length) {\n routeContext.i18n = picked\n }\n }\n\n if (\n requestInfo.pageType &&\n PageTypeSchema.safeParse(requestInfo.pageType).success\n ) {\n routeContext.pageType = requestInfo.pageType\n }\n\n if (\n typeof requestInfo.handle === 'string' &&\n requestInfo.handle &&\n requestInfo.handle.length <= MAX_ROUTE_CONTEXT_HANDLE_LENGTH\n ) {\n routeContext.handle = requestInfo.handle\n }\n\n return routeContext\n}\n\n/**\n * Studio per-item loader revalidation: POST the unsaved draft item (and, when\n * the runtime exposes route context, a sanitized `routeContext`) to the app's\n * revalidate route (see `createWeaverseNextRevalidateHandler`) and apply the\n * returned `loaderData` to the live item instance in place.\n *\n * No `router.refresh()` is involved, so the RSC tree is never re-applied — no\n * page remount, no scroll reset, and the draft payload never touches the URL.\n *\n * Throws on any failure (missing route, non-OK response, unknown item) so the\n * Builder Studio bridge can detect the miss and fall back to its legacy\n * refresh-based revalidation for this window.\n */\nexport async function revalidateWeaverseNextItem(\n runtime: RevalidateItemRuntimeLike,\n draftItem: WeaverseNextComponentData,\n endpoint: string = DEFAULT_REVALIDATE_ENDPOINT\n): Promise<void> {\n let instance = runtime.itemInstances.get(draftItem.id)\n if (!instance) {\n throw new Error(\n `[weaverse-next] No live item instance for \"${draftItem.id}\".`\n )\n }\n\n // Snapshot route identity at revalidation start so a later navigation cannot\n // retarget this in-flight request. Omit `routeContext` for legacy runtimes.\n let routeContext = buildWeaverseNextRevalidateRouteContext(\n runtime.requestInfo\n )\n let body = routeContext ? { draftItem, routeContext } : { draftItem }\n\n let response = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n })\n if (!response.ok) {\n throw new Error(\n `[weaverse-next] Revalidate endpoint responded ${response.status}.`\n )\n }\n\n let { loaderData } = (await response.json()) as { loaderData?: unknown }\n // The item's settings were already mutated by the Studio edit; only the\n // loader payload needs applying. `setData` merges and notifies subscribers,\n // and the new `loaderData` identity is what Builder's revalidation poll\n // observes for completion.\n instance.setData({ loaderData: loaderData ?? null })\n}\n","import type { PageSEOData } from '@weaverse/schema'\nimport type { Metadata } from 'next'\nimport type { WeaverseNextLoaderData, WeaverseNextPageData } from './types'\n\n/** Object containing an optional Weaverse page for SEO extraction. */\nexport interface WeaverseNextSeoPageContainer {\n /** Page whose published SEO fields should be formatted. */\n page?: WeaverseNextPageData | null\n}\n\n/** Input accepted by {@link getWeaverseNextSeoMetadata}. */\nexport type WeaverseNextSeoInput =\n | WeaverseNextLoaderData\n | WeaverseNextPageData\n | WeaverseNextSeoPageContainer\n | null\n | undefined\n\n/** Open Graph metadata accepted by Next.js's `Metadata` API. */\nexport type NextOpenGraph = NonNullable<Metadata['openGraph']>\n\n/** Twitter card metadata accepted by Next.js's `Metadata` API. */\nexport type NextTwitter = NonNullable<Metadata['twitter']>\n\n/**\n * Weaverse page SEO expressed as a subset of Next's `Metadata`. Assignable to\n * `Metadata` as-is: `const metadata: Metadata = getWeaverseNextSeoMetadata(data)`.\n */\nexport interface WeaverseNextSeoMetadata {\n /** Canonical URL settings for Next's metadata API. */\n alternates?: {\n /** Preferred URL for search indexing. */\n canonical?: string\n }\n /** Page meta description. */\n description?: string\n /** Page meta keywords. */\n keywords?: string\n /** Open Graph metadata normalized to Next's supported union. */\n openGraph?: NextOpenGraph\n /** Search crawler indexing and link-following directives. */\n robots: {\n /** Whether crawlers may follow links on the page. */\n follow: boolean\n /** Whether crawlers may index the page. */\n index: boolean\n }\n /** Page title. */\n title?: string\n /** Twitter card metadata normalized for Next's metadata API. */\n twitter?: NextTwitter\n}\n\nfunction hasOpenGraphContent(seo: PageSEOData['openGraph']): boolean {\n return Boolean(seo?.title || seo?.description || seo?.image || seo?.type)\n}\n\nfunction hasTwitterContent(seo: PageSEOData['twitter']): boolean {\n return Boolean(seo?.title || seo?.description || seo?.image || seo?.cardType)\n}\n\nfunction normalizeTwitterCard(\n cardType: NonNullable<PageSEOData['twitter']>['cardType']\n): 'summary' | 'summary_large_image' {\n return cardType === 'summary_large_image' ? 'summary_large_image' : 'summary'\n}\n\n/**\n * Map a Builder Open Graph type onto Next's `OpenGraphType` union. Every\n * Builder value except `product` exists in Next's union and is preserved\n * verbatim; `product` is not modeled by Next and makes `generateMetadata()`\n * throw, so it degrades to `website` — the closest renderable equivalent.\n */\nfunction normalizeOpenGraphType(\n type: NonNullable<PageSEOData['openGraph']>['type']\n): 'article' | 'profile' | 'video.other' | 'website' {\n if (type === 'article' || type === 'profile' || type === 'video.other') {\n return type\n }\n return 'website'\n}\n\n/**\n * Convert Builder SEO fields into metadata accepted by Next.js.\n *\n * Unsupported Open Graph product types and incomplete Twitter app/player\n * cards are safely downgraded to renderable Next.js variants.\n */\nexport function formatWeaverseNextSeoMetadata(\n seo: PageSEOData | null | undefined\n): WeaverseNextSeoMetadata {\n let index = seo?.robots?.index !== false\n let follow = seo?.robots?.follow !== false\n let metadata: WeaverseNextSeoMetadata = {\n robots: { index, follow },\n }\n\n if (!seo) {\n return metadata\n }\n\n if (seo.title) {\n metadata.title = seo.title\n }\n if (seo.description) {\n metadata.description = seo.description\n }\n if (seo.keywords) {\n metadata.keywords = seo.keywords\n }\n if (seo.canonicalUrl) {\n metadata.alternates = { canonical: seo.canonicalUrl }\n }\n\n let openGraph = seo.openGraph\n if (hasOpenGraphContent(openGraph)) {\n metadata.openGraph = {\n title: openGraph?.title,\n description: openGraph?.description,\n images: openGraph?.image ? [openGraph.image] : undefined,\n type: normalizeOpenGraphType(openGraph?.type),\n }\n }\n\n let twitter = seo.twitter\n if (hasTwitterContent(twitter)) {\n // Builder does not collect the descriptors Next requires for `app` cards\n // (Next throws when they are missing) or `player` cards (which render an\n // empty, useless card without a player URL and dimensions). Downgrade both\n // to `summary` rather than emitting metadata Next cannot render.\n metadata.twitter = {\n card: normalizeTwitterCard(twitter?.cardType),\n title: twitter?.title,\n description: twitter?.description,\n images: twitter?.image ? [twitter.image] : undefined,\n }\n }\n\n return metadata\n}\n\n/**\n * Read page SEO from loader data or a page payload and format it for Next.js\n * `generateMetadata`. Missing data returns indexable/followable robot defaults.\n */\nexport function getWeaverseNextSeoMetadata(\n data: WeaverseNextSeoInput\n): WeaverseNextSeoMetadata {\n if (!data) {\n return formatWeaverseNextSeoMetadata(null)\n }\n let page =\n 'items' in data && 'id' in data\n ? (data as WeaverseNextPageData)\n : ((data as WeaverseNextSeoPageContainer).page ?? null)\n return formatWeaverseNextSeoMetadata(page?.seo ?? null)\n}\n"]}
|