@weaverse/next 0.1.0-alpha.9 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +161 -73
  2. package/dist/{chunk-6PMZ77RA.mjs → chunk-OSC5HR5V.mjs} +117 -8
  3. package/dist/chunk-OSC5HR5V.mjs.map +1 -0
  4. package/dist/client-boundary.js +1425 -0
  5. package/dist/client-boundary.js.map +1 -0
  6. package/dist/client-boundary.mjs +1395 -0
  7. package/dist/client-boundary.mjs.map +1 -0
  8. package/dist/index.js +292 -1003
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +90 -966
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/server.js +321 -91
  13. package/dist/server.js.map +1 -1
  14. package/dist/server.mjs +205 -28
  15. package/dist/server.mjs.map +1 -1
  16. package/dist/types/client-boundary.d.ts +25 -0
  17. package/dist/types/client-boundary.d.ts.map +1 -0
  18. package/dist/types/client.d.ts +16 -0
  19. package/dist/types/client.d.ts.map +1 -0
  20. package/dist/types/hooks.d.ts +20 -0
  21. package/dist/types/hooks.d.ts.map +1 -0
  22. package/dist/types/index.d.ts +33 -0
  23. package/dist/types/index.d.ts.map +1 -0
  24. package/dist/types/item.d.ts +60 -0
  25. package/dist/types/item.d.ts.map +1 -0
  26. package/dist/types/loader.d.ts +25 -0
  27. package/dist/types/loader.d.ts.map +1 -0
  28. package/dist/types/main.d.ts +11 -0
  29. package/dist/types/main.d.ts.map +1 -0
  30. package/dist/types/provider.d.ts +82 -0
  31. package/dist/types/provider.d.ts.map +1 -0
  32. package/dist/types/registry.d.ts +15 -0
  33. package/dist/types/registry.d.ts.map +1 -0
  34. package/dist/types/renderer.d.ts +21 -0
  35. package/dist/types/renderer.d.ts.map +1 -0
  36. package/dist/types/request-info.d.ts +19 -0
  37. package/dist/types/request-info.d.ts.map +1 -0
  38. package/dist/types/revalidate-item.d.ts +82 -0
  39. package/dist/types/revalidate-item.d.ts.map +1 -0
  40. package/dist/types/root-provider.d.ts +62 -0
  41. package/dist/types/root-provider.d.ts.map +1 -0
  42. package/dist/types/runtime.d.ts +113 -0
  43. package/dist/types/runtime.d.ts.map +1 -0
  44. package/dist/types/seo.d.ts +55 -0
  45. package/dist/types/seo.d.ts.map +1 -0
  46. package/dist/types/server/configs.d.ts +27 -0
  47. package/dist/types/server/configs.d.ts.map +1 -0
  48. package/dist/types/server/normalize-page-url.d.ts +19 -0
  49. package/dist/types/server/normalize-page-url.d.ts.map +1 -0
  50. package/dist/types/server/revalidate-handler.d.ts +55 -0
  51. package/dist/types/server/revalidate-handler.d.ts.map +1 -0
  52. package/dist/types/server/server-client.d.ts +21 -0
  53. package/dist/types/server/server-client.d.ts.map +1 -0
  54. package/dist/types/server.d.ts +19 -0
  55. package/dist/types/server.d.ts.map +1 -0
  56. package/dist/types/studio-bridge.d.ts +16 -0
  57. package/dist/types/studio-bridge.d.ts.map +1 -0
  58. package/dist/types/studio-connect.d.ts +27 -0
  59. package/dist/types/studio-connect.d.ts.map +1 -0
  60. package/dist/types/studio-router.d.ts +50 -0
  61. package/dist/types/studio-router.d.ts.map +1 -0
  62. package/dist/types/studio-script-src.d.ts +12 -0
  63. package/dist/types/studio-script-src.d.ts.map +1 -0
  64. package/dist/types/theme-settings-store.d.ts +18 -0
  65. package/dist/types/theme-settings-store.d.ts.map +1 -0
  66. package/dist/types/translate.d.ts +56 -0
  67. package/dist/types/translate.d.ts.map +1 -0
  68. package/dist/types/translation-context.d.ts +109 -0
  69. package/dist/types/translation-context.d.ts.map +1 -0
  70. package/dist/types/translation-store.d.ts +44 -0
  71. package/dist/types/translation-store.d.ts.map +1 -0
  72. package/dist/types/types.d.ts +665 -0
  73. package/dist/types/types.d.ts.map +1 -0
  74. package/dist/types/use-weaverse-next-studio.d.ts +34 -0
  75. package/dist/types/use-weaverse-next-studio.d.ts.map +1 -0
  76. package/dist/types/utils.d.ts +22 -0
  77. package/dist/types/utils.d.ts.map +1 -0
  78. package/package.json +12 -27
  79. package/src/client-boundary.ts +50 -0
  80. package/src/client.ts +102 -0
  81. package/src/hooks.ts +50 -0
  82. package/src/index.ts +143 -0
  83. package/src/item.ts +167 -0
  84. package/src/loader.ts +135 -0
  85. package/src/main.tsx +45 -0
  86. package/src/provider.tsx +223 -0
  87. package/src/registry.ts +63 -0
  88. package/src/renderer.tsx +100 -0
  89. package/src/request-info.ts +124 -0
  90. package/src/revalidate-item.ts +211 -0
  91. package/src/root-provider.tsx +180 -0
  92. package/src/runtime.ts +555 -0
  93. package/src/seo.ts +157 -0
  94. package/src/server/configs.ts +178 -0
  95. package/src/server/normalize-page-url.ts +73 -0
  96. package/src/server/revalidate-handler.ts +357 -0
  97. package/src/server/server-client.ts +728 -0
  98. package/src/server.ts +67 -0
  99. package/src/studio-bridge.tsx +82 -0
  100. package/src/studio-connect.tsx +88 -0
  101. package/src/studio-router.ts +85 -0
  102. package/src/studio-script-src.ts +128 -0
  103. package/src/theme-settings-store.ts +47 -0
  104. package/src/translate.ts +125 -0
  105. package/src/translation-context.tsx +182 -0
  106. package/src/translation-store.ts +69 -0
  107. package/src/types.ts +753 -0
  108. package/src/use-weaverse-next-studio.tsx +106 -0
  109. package/src/utils.ts +49 -0
  110. package/dist/chunk-6PMZ77RA.mjs.map +0 -1
  111. package/dist/index.d.mts +0 -580
  112. package/dist/index.d.ts +0 -580
  113. package/dist/seo-Dos9t22_.d.mts +0 -477
  114. package/dist/seo-Dos9t22_.d.ts +0 -477
  115. package/dist/server.d.mts +0 -99
  116. 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. Is the POC bootstrapped from Shopify's Hydrogen preview docs?
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
- ### 5. Is there a global app load context like Hydrogen/Pilot?
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
- In the POC that helper is `getWeaverseServerClient(...)` in `app/weaverse-next/server.ts`. It plays the same role as Hydrogen's global load context, but explicitly:
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)
@@ -104,11 +81,15 @@ So the mental model is still `weaverse.loadPage(...)`; only the injection mechan
104
81
 
105
82
  | Import | Use in | Purpose |
106
83
  | --- | --- | --- |
107
- | `@weaverse/next` | Client and shared code | Component registry, provider, renderer, hooks, Studio runtime/bind components, schema re-exports. |
84
+ | `@weaverse/next` | Server and Client Components | Component registry, provider, renderer, hooks, Studio runtime/bind components, schema re-exports. |
108
85
  | `@weaverse/next/server` | Server only | Real Weaverse API client, config resolution, page URL normalization, per-item revalidation route handler. |
109
86
 
110
87
  The server API lives under `@weaverse/next/server` so client components can safely import the root entry without pulling server-only fetch code into the client bundle.
111
88
 
89
+ The root entry is safe to import from Server Components (since `1.0.1`). Its components, hooks and runtime ship as a separate `'use client'` module (`dist/client-boundary.*`), so a Server Component can render `WeaverseNextRootProvider` or `WeaverseNextStudioConnect` directly, and server code gets the real `createSchema`, `generateDataFromSchema`, `runWeaverseComponentLoaders`, SEO helpers, translate helpers (`createTranslate`, `getNestedKey`, `interpolate`) and `isBrowser`/`isIframe` instead of client references.
90
+
91
+ These are client-only: from a Server Component they are client references you can render or pass down but not call. Components and providers (`WeaverseNextRootProvider`, `WeaverseNextProvider`, `WeaverseNextRenderer`, `WeaverseNextStudio`, `WeaverseNextStudioBridge`, `WeaverseNextStudioConnect`, `TranslationProvider`), hooks (`useThemeSettings`, `useTranslation`, `useWeaverse`, `useItemInstance`, `useParentInstance`, `useChildInstances`, `useWeaversePageData`, `useWeaverseRootData`, `useWeaverseCommerce`, `useWeaverseNextStudioInternals`), and the client runtime (`createWeaverseNextClient`, `createWeaverseNextRuntime`, `bindWeaverseNextStudioRuntime`, `WeaverseNextRuntime`, `loadWeaverseNextStudioScript`).
92
+
112
93
  ## Package structure
113
94
 
114
95
  ```text
@@ -152,7 +133,9 @@ Main exports:
152
133
  - `revalidateWeaverseNextItem()` — client-side per-item revalidation helper wired by `WeaverseNextStudio`.
153
134
  - Hooks: `useThemeSettings`, `useWeaversePageData`, `useWeaverseRootData`, `useWeaverseCommerce`.
154
135
  - Re-exported migration helpers from `@weaverse/react`: `useWeaverse`, `useParentInstance`, `useItemInstance`, `useChildInstances`.
155
- - `createSchema` and schema types from `@weaverse/schema`.
136
+ - Full `@weaverse/schema` surface, including `createSchema`, schema types, validators, and schema helper utilities.
137
+ - `generateDataFromSchema(schema)` — framework-neutral default-setting extraction utility shared with component/theme schema flows.
138
+ - Public `WeaverseNextThemeSchema` type for root/server theme settings config.
156
139
 
157
140
  ## Server API (`@weaverse/next/server`)
158
141
 
@@ -168,15 +151,26 @@ Main exports:
168
151
 
169
152
  Example helper for a Next route/server component boundary:
170
153
 
154
+ This helper is app-level (not exported by `@weaverse/next`); it plays the role
155
+ of Hydrogen's global load context, but the app owns route identity explicitly.
156
+ The App Router boundary supplies `pathname`, `pageType`, and `handle`, so the
157
+ same identity reaches page loads and per-item revalidation loaders. It uses the
158
+ exported `PageType` and `WeaverseNextRequestContext` types. The module defines
159
+ both helpers used by the route-handler example below:
160
+
171
161
  ```ts
172
162
  import { createWeaverseNextServerClient } from '@weaverse/next/server'
163
+ import type { PageType, WeaverseNextRequestContext } from '@weaverse/next'
173
164
  import { headers } from 'next/headers'
174
165
  import { serverComponents } from './server-components'
175
166
  import { getStaticStorefrontClient } from './storefront'
176
167
 
177
168
  export async function getWeaverseServerClient(
178
169
  searchParamsPromise: Promise<Record<string, string | string[] | undefined>>,
179
- pathname = '/'
170
+ pathname = '/',
171
+ // Route identity the App Router segment owns. Seeding it here means loaders
172
+ // resolve the same page type / handle during SSR and per-item revalidation.
173
+ identity: { pageType?: PageType; handle?: string } = {}
180
174
  ) {
181
175
  let [headersList, rawSearchParams] = await Promise.all([
182
176
  headers(),
@@ -200,6 +194,27 @@ export async function getWeaverseServerClient(
200
194
  }`
201
195
  )
202
196
 
197
+ let requestContext: WeaverseNextRequestContext = {
198
+ url,
199
+ headers: requestHeaders,
200
+ searchParams,
201
+ pathname,
202
+ pageType: identity.pageType,
203
+ handle: identity.handle,
204
+ i18n: { country: 'US', language: 'EN', locale: 'en-US' },
205
+ }
206
+
207
+ return createWeaverseServerClientFromContext(requestContext)
208
+ }
209
+
210
+ // Explicit-context path used by the revalidation callback. The context has
211
+ // already been validated by the SDK handler; project/host/env remain app-owned.
212
+ export function createWeaverseServerClientFromContext(
213
+ requestContext: WeaverseNextRequestContext
214
+ ) {
215
+ // A localized app should select/cache this client from its own supported
216
+ // market allowlist using requestContext.i18n. This compact example uses the
217
+ // default market configured by the app helper.
203
218
  let storefront = getStaticStorefrontClient()
204
219
 
205
220
  return createWeaverseNextServerClient({
@@ -218,32 +233,42 @@ export async function getWeaverseServerClient(
218
233
  },
219
234
  }
220
235
  : undefined,
221
- requestContext: {
222
- url,
223
- headers: requestHeaders,
224
- searchParams,
225
- pathname,
226
- i18n: { country: 'US', language: 'EN', locale: 'en-US' },
227
- },
236
+ requestContext,
228
237
  cache: { revalidate: 60, tags: ['weaverse'] },
229
238
  })
230
239
  }
231
240
  ```
232
241
 
233
- Then each route can stay close to the Hydrogen mental model:
242
+ Then each route stays close to the Hydrogen mental model while supplying the
243
+ route identity the segment owns — an `INDEX` home route and a handle-bearing
244
+ `PRODUCT` route:
234
245
 
235
246
  ```ts
236
- let weaverse = await getWeaverseServerClient(props.searchParams, '/')
247
+ // app/page.tsx — the INDEX segment owns the page type.
248
+ let weaverse = await getWeaverseServerClient(props.searchParams, '/', {
249
+ pageType: 'INDEX',
250
+ })
237
251
  let data = await weaverse.loadPage({ type: 'INDEX' })
238
252
  let theme = await weaverse.loadThemeSettings()
239
253
  ```
240
254
 
255
+ ```ts
256
+ // app/products/[handle]/page.tsx — the PRODUCT segment supplies the handle.
257
+ let { handle } = await props.params
258
+ let weaverse = await getWeaverseServerClient(
259
+ props.searchParams,
260
+ `/products/${handle}`,
261
+ { pageType: 'PRODUCT', handle }
262
+ )
263
+ let data = await weaverse.loadPage({ type: 'PRODUCT', handle })
264
+ ```
265
+
241
266
  ### Server client members
242
267
 
243
268
  | Member | Description |
244
269
  | --- | --- |
245
270
  | `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. |
271
+ | `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
272
  | `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
273
  | `fetchWithCache<T>(url, options?)` | Next-aware fetch helper. Uses `cache: 'no-store'` in design/revision preview and `next: { revalidate, tags }` in published mode. |
249
274
  | `resolveProjectId()` / `projectId` | Resolves project id from Studio query, config function/string, or env. |
@@ -282,12 +307,13 @@ Use the server entry in `generateMetadata()` to map Weaverse `page.seo` into a N
282
307
 
283
308
  ```ts
284
309
  // app/pages/[handle]/page.tsx
310
+ import type { Metadata } from 'next'
285
311
  import { getWeaverseNextSeoMetadata } from '@weaverse/next/server'
286
312
 
287
313
  export async function generateMetadata(props: {
288
314
  params: Promise<{ handle: string }>
289
315
  searchParams: Promise<Record<string, string | string[] | undefined>>
290
- }) {
316
+ }): Promise<Metadata> {
291
317
  let { handle } = await props.params
292
318
  let weaverse = await getWeaverseServerClient(props.searchParams, `/pages/${handle}`)
293
319
  let data = await weaverse.loadPage({ type: 'PAGE', handle })
@@ -295,7 +321,17 @@ export async function generateMetadata(props: {
295
321
  }
296
322
  ```
297
323
 
298
- The helper is pure and does not import `next/*`; its return shape is intentionally structural so the app can type it as `Metadata` if desired.
324
+ The helper is pure and has no runtime dependency on `next`; it only imports the
325
+ `Metadata` type. Its return value is directly assignable to Next `Metadata`.
326
+
327
+ Two Builder-only SEO values are normalized so Next can always render the result:
328
+
329
+ - Open Graph type `product` falls back to `website` (Next throws on `product`).
330
+ The other Builder types — `website`, `article`, `profile`, `video.other` — are
331
+ passed through unchanged.
332
+ - Twitter card types `app` and `player` fall back to `summary`, because Builder
333
+ does not collect the extra descriptors those cards need (Next throws without
334
+ the `app` fields, and a `player` card without a player URL renders empty).
299
335
 
300
336
  ### Custom pages for sitemap generation
301
337
 
@@ -336,16 +372,18 @@ import {
336
372
  WeaverseNextProvider,
337
373
  WeaverseNextRenderer,
338
374
  } from '@weaverse/next'
339
- import type { WeaverseNextLoaderData } from '@weaverse/next'
375
+ import type {
376
+ WeaverseNextLoaderData,
377
+ WeaverseNextRequestInfo,
378
+ } from '@weaverse/next'
340
379
  import { components } from './components'
341
380
 
342
381
  export function WeaversePage({ data }: { data: WeaverseNextLoaderData }) {
382
+ // The server client returns `requestInfo` in the exported shape, so restore
383
+ // the full route identity — including `pageType` and `handle` — into the
384
+ // client request context instead of a narrow inline subset.
343
385
  let requestInfo = data.configs?.requestInfo as
344
- | {
345
- i18n?: { country?: string; language?: string; locale?: string }
346
- pathname?: string
347
- search?: string
348
- }
386
+ | WeaverseNextRequestInfo
349
387
  | undefined
350
388
  let projectId =
351
389
  (data.configs?.projectId as string | undefined) ?? data.project?.id
@@ -361,6 +399,8 @@ export function WeaversePage({ data }: { data: WeaverseNextLoaderData }) {
361
399
  isPreviewMode: Boolean(data.configs?.isPreviewMode),
362
400
  isRevisionPreview: Boolean(data.configs?.isRevisionPreview),
363
401
  i18n: requestInfo?.i18n,
402
+ pageType: requestInfo?.pageType,
403
+ handle: requestInfo?.handle,
364
404
  },
365
405
  })
366
406
 
@@ -479,13 +519,38 @@ The root script connector and page-level renderer are both required for full Stu
479
519
 
480
520
  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
521
 
482
- Mount the route handler in the consuming app:
522
+ Mount the route handler in the consuming app. `getClient` receives a second
523
+ argument — the validated, same-origin route context reconstructed from the
524
+ request body — so the revalidated loader runs with the same pathname, locale,
525
+ page type, handle, and ordinary search params as the active storefront route.
526
+ The two server-client helpers below are app-level consumer helpers, not exports
527
+ from `@weaverse/next`:
483
528
 
484
529
  ```ts
485
530
  // app/api/weaverse/revalidate/route.ts
486
531
  import { createWeaverseNextRevalidateHandler } from '@weaverse/next/server'
487
- import { getWeaverseServerClient } from '../../../weaverse-next/server'
532
+ import {
533
+ createWeaverseServerClientFromContext,
534
+ getWeaverseServerClient,
535
+ } from '../../../weaverse-next/server'
536
+
537
+ export const { POST } = createWeaverseNextRevalidateHandler({
538
+ // `requestContext` is validated browser input for route identity only.
539
+ // Project ID, Studio host, API base, and env must still come from
540
+ // server config — never from this context — and it is `undefined` for
541
+ // legacy request bodies that carry no route context.
542
+ getClient: (_request, requestContext) =>
543
+ requestContext
544
+ ? createWeaverseServerClientFromContext(requestContext)
545
+ : getWeaverseServerClient(Promise.resolve({})),
546
+ })
547
+ ```
488
548
 
549
+ The existing one-argument callback still works. A handler mounted with a
550
+ `(request) => client` callback ignores the optional second argument and keeps
551
+ compiling and running, so old wiring needs no change:
552
+
553
+ ```ts
489
554
  export const { POST } = createWeaverseNextRevalidateHandler({
490
555
  getClient: () => getWeaverseServerClient(Promise.resolve({})),
491
556
  })
@@ -495,15 +560,46 @@ Flow:
495
560
 
496
561
  ```text
497
562
  Studio resource picker edit
498
- -> Builder calls runtime.internal.revalidateItem(draftItem)
499
- -> SDK POSTs { draftItem } to /api/weaverse/revalidate
563
+ -> Builder calls runtime.internal.revalidateItem(draftItem) (unchanged)
564
+ -> SDK snapshots runtime.requestInfo, strips server-owned/transient controls
565
+ -> SDK POSTs { draftItem, routeContext? } to /api/weaverse/revalidate
566
+ -> handler validates + sanitizes routeContext at the trust boundary
567
+ -> handler reconstructs a same-origin WeaverseNextRequestContext
568
+ -> getClient(request, requestContext) builds the route-aware server client
500
569
  -> route handler finds the registered component by draftItem.type
501
- -> handler runs that component's loader with draft item data
502
- -> response returns { loaderData }
570
+ -> handler runs that component's loader with draft data + client.requestContext
571
+ -> response returns { loaderData } with Cache-Control: no-store
503
572
  -> SDK applies loaderData to the live item instance in place
504
573
  ```
505
574
 
506
- If the route is missing or returns an error, Builder falls back to the older route-refresh path.
575
+ If the route is missing or returns an error, Builder falls back to the older
576
+ route-refresh path.
577
+
578
+ ### Trust boundary
579
+
580
+ The revalidation endpoint is public and every JSON field is attacker-controlled.
581
+ The SDK sanitizes `routeContext` on the client for clean traffic, but the route
582
+ handler is the security boundary and re-validates independently:
583
+
584
+ - Only `pathname`, sanitized `search`, a narrow i18n subset, a
585
+ `PageTypeSchema`-valid `pageType`, and a bounded `handle` cross the boundary.
586
+ Headers, cookies, auth, env, project ID, Studio host, API base, commerce
587
+ clients, the runtime, and the client are never serialized.
588
+ - Server-owned controls (`weaverseProjectId`, `weaverseHost`, `weaverseApiBase`,
589
+ `weaversePublicApiBase`, `weaverseVersion`, `projectId`) and transient transport
590
+ controls (`weaverseDraftItem`, `__weaverseDraftItem`, `_rsc`) are stripped
591
+ case-insensitively on both sides, so a crafted body cannot influence server
592
+ config resolution.
593
+ - The origin is fixed from the endpoint request before assigning any
594
+ browser-provided pathname/search, so input cannot change protocol, host, port,
595
+ or credentials. `pathname` and `url.pathname` share one URL-canonicalized value.
596
+ - A malformed present `routeContext` returns `400 { error: 'invalid-route-context' }`
597
+ before `getClient` runs; a missing `routeContext` is valid legacy input and
598
+ passes `undefined` to the callback.
599
+ - Route context is routing input, never an authorization decision. A registered
600
+ loader must not treat pathname, handle, page type, locale, or search as proof
601
+ of access; customer/private data still requires independent server-side session
602
+ and authorization checks derived from the actual request.
507
603
 
508
604
  ## Config resolution
509
605
 
@@ -514,33 +610,25 @@ Server config resolution intentionally mirrors Hydrogen where possible:
514
610
  - API base: trusted request host → `WEAVERSE_PUBLIC_API_BASE` → non-production `WEAVERSE_HOST` → `https://api.weaverse.io`.
515
611
  - public env: `PUBLIC_STORE_DOMAIN`, `PUBLIC_STOREFRONT_API_TOKEN`.
516
612
 
517
- `WEAVERSE_API_KEY` may be read into internal base configs but is not attached to page/theme API requests and is never serialized into client-facing loader data.
613
+ ## Alpha migration notes
518
614
 
519
- ## POC reference
615
+ ### After `0.1.0-alpha.9`
520
616
 
521
- Live POC:
617
+ - 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.
618
+ - 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.
619
+ - `generateDataFromSchema` is public from the root entry and accepts component schemas or `WeaverseNextThemeSchema`.
620
+ - Version bumping still happens in the release flow; do not publish another alpha without bumping `packages/next/package.json` first.
522
621
 
523
- - https://weaverse-hydrogen-next-poc.vercel.app
622
+ ### Bundle note
524
623
 
525
- Repo:
624
+ The root entry is built as two files: `dist/index.*` (server-safe exports) and `dist/client-boundary.*` (client code under a `'use client'` banner, imported by name from `dist/index.*`). Bundling drops module-level directives, so the banner is applied to the built boundary file; `scripts/check-packed-packages.mjs` fails the package check if the boundary loses it or if any other file reaches React client APIs.
526
625
 
527
- - https://github.com/Weaverse/weaverse-hydrogen-next-poc
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
- ```
626
+ 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
627
 
541
628
  ## Current limitations
542
629
 
543
630
  - This package is still alpha; API names may change before stable.
544
631
  - Next does not provide Hydrogen's global React Router loader context, so apps should keep a small `getWeaverseServerClient(...)` helper.
545
- - Translation/static-text sidecar parity, multiple Weaverse runtimes on one URL, and global sections are later hardening items.
632
+ - 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.
633
+ - Global sections are Builder-owned; `@weaverse/next` should only need fixes if manual Studio QA finds a rendering/runtime boundary issue.
546
634
  - 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 = {};
@@ -15,6 +17,8 @@ function generateDataFromSchema(schema) {
15
17
  }
16
18
  return data;
17
19
  }
20
+ var isBrowser = typeof window !== "undefined";
21
+ var isIframe = isBrowser && window.top !== window.self;
18
22
 
19
23
  // src/loader.ts
20
24
  function isItemNode(node) {
@@ -107,13 +111,22 @@ function getSearchParamsFromContext(context) {
107
111
  }
108
112
  return new URLSearchParams();
109
113
  }
114
+ function withPathPrefix(pathname, i18n) {
115
+ let raw = typeof i18n?.pathPrefix === "string" ? i18n.pathPrefix : "";
116
+ let segments = raw.split("/").filter(Boolean);
117
+ let prefix = segments.length > 0 ? `/${segments.join("/")}` : "";
118
+ if (prefix === "" || pathname === prefix || pathname.startsWith(`${prefix}/`)) {
119
+ return pathname;
120
+ }
121
+ return pathname === "/" ? prefix : `${prefix}${pathname}`;
122
+ }
110
123
  function getPathnameFromContext(context) {
111
124
  if (context?.pathname) {
112
- return context.pathname;
125
+ return withPathPrefix(context.pathname, context.i18n);
113
126
  }
114
127
  if (context?.url) {
115
128
  let url = toUrl(context.url);
116
- return url.pathname || "/";
129
+ return withPathPrefix(url.pathname || "/", context.i18n);
117
130
  }
118
131
  return "/";
119
132
  }
@@ -154,9 +167,96 @@ function buildWeaverseNextRequestInfo(context) {
154
167
  pathname: getPathnameFromContext(context),
155
168
  search: search ? `?${search}` : "",
156
169
  queries: toQueryRecord(searchParams),
157
- ...context?.i18n ? { i18n: context.i18n } : {}
170
+ ...context?.i18n ? { i18n: context.i18n } : {},
171
+ ...context?.pageType ? { pageType: context.pageType } : {},
172
+ ...context?.handle ? { handle: context.handle } : {}
158
173
  };
159
174
  }
175
+ var DEFAULT_REVALIDATE_ENDPOINT = "/api/weaverse/revalidate";
176
+ var MAX_ROUTE_CONTEXT_HANDLE_LENGTH = 512;
177
+ var DENIED_ROUTE_CONTEXT_PARAMS = [
178
+ "weaverseprojectid",
179
+ "weaversehost",
180
+ "weaverseapikey",
181
+ "weaverseapibase",
182
+ "weaversepublicapibase",
183
+ "weaverseversion",
184
+ "projectid",
185
+ "weaversedraftitem",
186
+ "__weaversedraftitem",
187
+ "_rsc"
188
+ ];
189
+ var ROUTE_CONTEXT_I18N_KEYS = [
190
+ "country",
191
+ "label",
192
+ "language",
193
+ "locale",
194
+ "pathPrefix"
195
+ ];
196
+ function sanitizeRouteContextSearch(search) {
197
+ let params = new URLSearchParams(search);
198
+ let denied = new Set(DENIED_ROUTE_CONTEXT_PARAMS);
199
+ for (let key of [...new Set(params.keys())]) {
200
+ if (denied.has(key.toLowerCase())) {
201
+ params.delete(key);
202
+ }
203
+ }
204
+ let serialized = params.toString();
205
+ return serialized ? `?${serialized}` : "";
206
+ }
207
+ function buildWeaverseNextRevalidateRouteContext(requestInfo) {
208
+ if (!requestInfo || typeof requestInfo.pathname !== "string" || !requestInfo.pathname) {
209
+ return;
210
+ }
211
+ let routeContext = {
212
+ pathname: requestInfo.pathname,
213
+ search: sanitizeRouteContextSearch(requestInfo.search ?? "")
214
+ };
215
+ let { i18n } = requestInfo;
216
+ if (i18n && typeof i18n === "object") {
217
+ let picked = {};
218
+ for (let key of ROUTE_CONTEXT_I18N_KEYS) {
219
+ let value = i18n[key];
220
+ if (typeof value === "string") {
221
+ picked[key] = value;
222
+ }
223
+ }
224
+ if (Object.keys(picked).length) {
225
+ routeContext.i18n = picked;
226
+ }
227
+ }
228
+ if (requestInfo.pageType && PageTypeSchema.safeParse(requestInfo.pageType).success) {
229
+ routeContext.pageType = requestInfo.pageType;
230
+ }
231
+ if (typeof requestInfo.handle === "string" && requestInfo.handle && requestInfo.handle.length <= MAX_ROUTE_CONTEXT_HANDLE_LENGTH) {
232
+ routeContext.handle = requestInfo.handle;
233
+ }
234
+ return routeContext;
235
+ }
236
+ async function revalidateWeaverseNextItem(runtime, draftItem, endpoint = DEFAULT_REVALIDATE_ENDPOINT) {
237
+ let instance = runtime.itemInstances.get(draftItem.id);
238
+ if (!instance) {
239
+ throw new Error(
240
+ `[weaverse-next] No live item instance for "${draftItem.id}".`
241
+ );
242
+ }
243
+ let routeContext = buildWeaverseNextRevalidateRouteContext(
244
+ runtime.requestInfo
245
+ );
246
+ let body = routeContext ? { draftItem, routeContext } : { draftItem };
247
+ let response = await fetch(endpoint, {
248
+ method: "POST",
249
+ headers: { "Content-Type": "application/json" },
250
+ body: JSON.stringify(body)
251
+ });
252
+ if (!response.ok) {
253
+ throw new Error(
254
+ `[weaverse-next] Revalidate endpoint responded ${response.status}.`
255
+ );
256
+ }
257
+ let { loaderData } = await response.json();
258
+ instance.setData({ loaderData: loaderData ?? null });
259
+ }
160
260
 
161
261
  // src/seo.ts
162
262
  function hasOpenGraphContent(seo) {
@@ -165,6 +265,15 @@ function hasOpenGraphContent(seo) {
165
265
  function hasTwitterContent(seo) {
166
266
  return Boolean(seo?.title || seo?.description || seo?.image || seo?.cardType);
167
267
  }
268
+ function normalizeTwitterCard(cardType) {
269
+ return cardType === "summary_large_image" ? "summary_large_image" : "summary";
270
+ }
271
+ function normalizeOpenGraphType(type) {
272
+ if (type === "article" || type === "profile" || type === "video.other") {
273
+ return type;
274
+ }
275
+ return "website";
276
+ }
168
277
  function formatWeaverseNextSeoMetadata(seo) {
169
278
  let index = seo?.robots?.index !== false;
170
279
  let follow = seo?.robots?.follow !== false;
@@ -192,13 +301,13 @@ function formatWeaverseNextSeoMetadata(seo) {
192
301
  title: openGraph?.title,
193
302
  description: openGraph?.description,
194
303
  images: openGraph?.image ? [openGraph.image] : void 0,
195
- type: openGraph?.type || "website"
304
+ type: normalizeOpenGraphType(openGraph?.type)
196
305
  };
197
306
  }
198
307
  let twitter = seo.twitter;
199
308
  if (hasTwitterContent(twitter)) {
200
309
  metadata.twitter = {
201
- card: twitter?.cardType || "summary",
310
+ card: normalizeTwitterCard(twitter?.cardType),
202
311
  title: twitter?.title,
203
312
  description: twitter?.description,
204
313
  images: twitter?.image ? [twitter.image] : void 0
@@ -214,6 +323,6 @@ function getWeaverseNextSeoMetadata(data) {
214
323
  return formatWeaverseNextSeoMetadata(page?.seo ?? null);
215
324
  }
216
325
 
217
- export { buildWeaverseNextRequestInfo, formatWeaverseNextSeoMetadata, generateDataFromSchema, getWeaverseNextSeoMetadata, runWeaverseComponentLoaders };
218
- //# sourceMappingURL=chunk-6PMZ77RA.mjs.map
219
- //# sourceMappingURL=chunk-6PMZ77RA.mjs.map
326
+ export { DEFAULT_REVALIDATE_ENDPOINT, ROUTE_CONTEXT_I18N_KEYS, buildWeaverseNextRequestInfo, buildWeaverseNextRevalidateRouteContext, formatWeaverseNextSeoMetadata, generateDataFromSchema, getWeaverseNextSeoMetadata, isBrowser, isIframe, revalidateWeaverseNextItem, runWeaverseComponentLoaders, sanitizeRouteContextSearch, withPathPrefix };
327
+ //# sourceMappingURL=chunk-OSC5HR5V.mjs.map
328
+ //# sourceMappingURL=chunk-OSC5HR5V.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-OSC5HR5V.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\n/**\n * Whether the code runs in a browser window.\n *\n * Defined here rather than re-exported from `@weaverse/react`, which sits\n * behind the `'use client'` boundary: there a Server Component would receive a\n * client reference, a truthy object, instead of `false`.\n */\nexport let isBrowser = typeof window !== 'undefined'\n\n/** Whether the code runs in a browser window embedded in an iframe. */\nexport let isIframe = isBrowser && window.top !== window.self\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"]}