@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
@@ -0,0 +1,728 @@
1
+ import type { SchemaType } from '@weaverse/schema'
2
+ import { runWeaverseComponentLoaders } from '../loader'
3
+ import { buildWeaverseNextRequestInfo } from '../request-info'
4
+ import type {
5
+ WeaverseNextBaseConfigs,
6
+ WeaverseNextCacheConfig,
7
+ WeaverseNextCommerceContext,
8
+ WeaverseNextComponent,
9
+ WeaverseNextComponentData,
10
+ WeaverseNextConfigs,
11
+ WeaverseNextCustomPageEntry,
12
+ WeaverseNextFetchCustomPagesOptions,
13
+ WeaverseNextFetchOptions,
14
+ WeaverseNextLoaderData,
15
+ WeaverseNextLoadPageInput,
16
+ WeaverseNextPageAssignment,
17
+ WeaverseNextPageData,
18
+ WeaverseNextProjectId,
19
+ WeaverseNextRequestContext,
20
+ WeaverseNextServerClient,
21
+ WeaverseNextServerClientConfig,
22
+ WeaverseNextServerLoadPageInput,
23
+ WeaverseNextStorefront,
24
+ WeaverseNextThemeSchema,
25
+ WeaverseNextThemeSettingsOptions,
26
+ WeaverseNextThemeSettingsResponse,
27
+ } from '../types'
28
+ import { generateDataFromSchema } from '../utils'
29
+ import { getWeaverseNextConfigs } from './configs'
30
+ import { normalizeNextPageUrl, resolveRequestUrl } from './normalize-page-url'
31
+
32
+ const DEFAULT_FETCH_TIMEOUT_MS = 10_000
33
+ const RANDOM_ID_RADIX = 36
34
+ const RANDOM_ID_SLICE_START = 2
35
+ const MAX_CUSTOM_PAGE_PAGES = 100
36
+
37
+ const JSON_HEADERS = {
38
+ 'Content-Type': 'application/json',
39
+ Accept: 'application/json',
40
+ 'Accept-Encoding': 'gzip, deflate, br',
41
+ } as const
42
+
43
+ /** Next's `fetch` reads `next: { revalidate, tags }` off the request init. */
44
+ type NextRequestInit = RequestInit & {
45
+ next?: { revalidate?: number | false; tags?: string[] }
46
+ }
47
+
48
+ type LooseThemeSchema = WeaverseNextThemeSchema
49
+
50
+ function asThemeSchema(schema: unknown): LooseThemeSchema | undefined {
51
+ return schema && typeof schema === 'object'
52
+ ? (schema as LooseThemeSchema)
53
+ : undefined
54
+ }
55
+
56
+ function generateUuid(): string {
57
+ if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
58
+ return crypto.randomUUID()
59
+ }
60
+ return `id-${Math.random().toString(RANDOM_ID_RADIX).slice(RANDOM_ID_SLICE_START)}`
61
+ }
62
+
63
+ /**
64
+ * Normalize the raw `pageAssignment` from `/api/public/project` into a
65
+ * save-compatible {@link WeaverseNextPageAssignment} so the Builder Studio save
66
+ * pipeline receives the same shape it gets from Hydrogen. Returns `undefined`
67
+ * for a missing or malformed assignment (rather than fabricating one), requires
68
+ * string `projectId`/`type`/`handle`, coerces a nullish `locale` to `''`, and
69
+ * preserves an object `meta` untouched.
70
+ */
71
+ function normalizeWeaverseNextPageAssignment(
72
+ input: unknown
73
+ ): WeaverseNextPageAssignment | undefined {
74
+ if (!input || typeof input !== 'object') {
75
+ return
76
+ }
77
+ let { projectId, type, handle, locale, meta } = input as Record<
78
+ string,
79
+ unknown
80
+ >
81
+ if (
82
+ typeof projectId !== 'string' ||
83
+ typeof type !== 'string' ||
84
+ typeof handle !== 'string'
85
+ ) {
86
+ return
87
+ }
88
+ let normalized: WeaverseNextPageAssignment = {
89
+ projectId,
90
+ type,
91
+ handle,
92
+ locale: typeof locale === 'string' ? locale : '',
93
+ }
94
+ if (meta && typeof meta === 'object') {
95
+ normalized.meta = meta as WeaverseNextPageAssignment['meta']
96
+ }
97
+ return normalized
98
+ }
99
+
100
+ /**
101
+ * Server-side Weaverse client for Next.js. Performs the real Weaverse public
102
+ * API fetch (page + theme settings) instead of delegating to injected fetchers,
103
+ * closing the gap with Hydrogen's `context.weaverse.loadPage(...)`.
104
+ */
105
+ class NextServerClient implements WeaverseNextServerClient {
106
+ components: WeaverseNextComponent[]
107
+ commerce?: WeaverseNextCommerceContext
108
+ requestContext?: WeaverseNextRequestContext
109
+ themeSchema?: SchemaType | WeaverseNextThemeSchema
110
+ themeSettings: Record<string, unknown>
111
+ data: WeaverseNextLoaderData | null = null
112
+ dataContext: Record<string, unknown> = {}
113
+ configs: WeaverseNextConfigs
114
+
115
+ private readonly _projectIdInput?: WeaverseNextProjectId
116
+ private readonly _baseConfigs: WeaverseNextBaseConfigs
117
+ private readonly _fetch: typeof fetch
118
+ private readonly _cache?: WeaverseNextServerClientConfig['cache']
119
+ private readonly _fetchTimeoutMs: number
120
+ private _resolvedProjectId?: string
121
+
122
+ constructor(config: WeaverseNextServerClientConfig) {
123
+ // Server client keeps the component definitions for server-side loaders
124
+ // (`runWeaverseComponentLoaders`) and preview-data generation, but it
125
+ // intentionally does NOT call `registerWeaverseNextComponents`. Registering
126
+ // pulls in `../registry` → `@weaverse/react`, which evaluates
127
+ // `React.createContext()` at module load — invalid in the RSC/server graph
128
+ // and the cause of `(0 , x.createContext) is not a function`. The client
129
+ // wrapper/renderer (root `@weaverse/next` client entry) registers the real
130
+ // render components instead, keeping `@weaverse/next/server` React-free.
131
+ this.components = config.components
132
+ this.commerce = config.commerce
133
+ this.requestContext = config.requestContext
134
+ this.themeSchema = config.themeSchema
135
+ // Seed schema defaults before merchant overrides, mirroring the root client.
136
+ this.themeSettings = {
137
+ ...generateDataFromSchema(config.themeSchema),
138
+ ...config.themeSettings,
139
+ }
140
+ this._projectIdInput = config.projectId
141
+ this._fetch = config.fetch ?? globalThis.fetch
142
+ this._cache = config.cache
143
+ this._fetchTimeoutMs = config.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS
144
+
145
+ this._baseConfigs = getWeaverseNextConfigs(config.requestContext, {
146
+ env: config.env,
147
+ weaverseHost: config.weaverseHost,
148
+ weaverseApiBase: config.weaverseApiBase,
149
+ weaverseVersion: config.weaverseVersion,
150
+ })
151
+
152
+ // Synchronous best-effort projectId (query → string → env). A function
153
+ // resolver is awaited lazily in `resolveProjectId`, so `projectId` reflects
154
+ // its result after the first `loadPage`/`loadThemeSettings`/`resolveProjectId`.
155
+ this.configs = this._buildPublicConfigs(
156
+ this._baseConfigs.queryProjectId ||
157
+ (typeof this._projectIdInput === 'string'
158
+ ? this._projectIdInput.trim()
159
+ : '') ||
160
+ this._baseConfigs.envProjectId
161
+ )
162
+ }
163
+
164
+ get projectId(): string {
165
+ return this.configs.projectId
166
+ }
167
+
168
+ /** Compatibility alias so loaders can call `weaverse.storefront.query(...)`. */
169
+ get storefront(): WeaverseNextStorefront | undefined {
170
+ return this.commerce?.storefront
171
+ }
172
+
173
+ private _buildPublicConfigs(projectId: string): WeaverseNextConfigs {
174
+ let base = this._baseConfigs
175
+ return {
176
+ projectId,
177
+ weaverseHost: base.weaverseHost,
178
+ weaverseApiBase: base.weaverseApiBase,
179
+ weaversePublicApiBase: base.weaverseApiBase,
180
+ weaverseVersion: base.weaverseVersion,
181
+ isDesignMode: base.isDesignMode,
182
+ isPreviewMode: base.isPreviewMode,
183
+ isRevisionPreview: base.isRevisionPreview,
184
+ sectionType: base.sectionType,
185
+ publicEnv: base.publicEnv,
186
+ }
187
+ }
188
+
189
+ resolveProjectId = async (): Promise<string> => {
190
+ if (this._resolvedProjectId !== undefined) {
191
+ return this._resolvedProjectId
192
+ }
193
+
194
+ // Priority: query param → function → string → env (matches Hydrogen).
195
+ let resolved = this._baseConfigs.queryProjectId
196
+ if (!resolved && typeof this._projectIdInput === 'function') {
197
+ try {
198
+ let value = await this._projectIdInput(this.requestContext)
199
+ if (typeof value === 'string' && value.trim()) {
200
+ resolved = value.trim()
201
+ }
202
+ } catch (error) {
203
+ console.warn(
204
+ '[WeaverseNextServerClient] projectId resolver threw, falling back.',
205
+ error
206
+ )
207
+ }
208
+ }
209
+ if (!resolved && typeof this._projectIdInput === 'string') {
210
+ resolved = this._projectIdInput.trim()
211
+ }
212
+ if (!resolved) {
213
+ resolved = this._baseConfigs.envProjectId
214
+ }
215
+
216
+ this._resolvedProjectId = resolved
217
+ this.configs = this._buildPublicConfigs(resolved)
218
+ return resolved
219
+ }
220
+
221
+ fetchWithCache = async <T = unknown>(
222
+ url: string,
223
+ options: WeaverseNextFetchOptions = {}
224
+ ): Promise<T> => {
225
+ let { revalidate, tags, ...init } = options
226
+ let requestInit: NextRequestInit = { ...init }
227
+
228
+ let { isDesignMode, isRevisionPreview } = this._baseConfigs
229
+ if (isDesignMode || isRevisionPreview) {
230
+ // Design / revision data must never be served stale.
231
+ requestInit.cache = 'no-store'
232
+ } else if (requestInit.cache === undefined) {
233
+ let effectiveRevalidate = revalidate ?? this._cache?.revalidate
234
+ let effectiveTags = tags ?? this._cache?.tags
235
+ if (effectiveRevalidate !== undefined || effectiveTags?.length) {
236
+ requestInit.next = {
237
+ ...(effectiveRevalidate === undefined
238
+ ? {}
239
+ : { revalidate: effectiveRevalidate }),
240
+ ...(effectiveTags?.length ? { tags: effectiveTags } : {}),
241
+ }
242
+ }
243
+ }
244
+
245
+ let controller = new AbortController()
246
+ let timeoutId = setTimeout(() => controller.abort(), this._fetchTimeoutMs)
247
+ try {
248
+ let response = await this._fetch(url, {
249
+ ...requestInit,
250
+ signal: requestInit.signal ?? controller.signal,
251
+ })
252
+ if (!response.ok) {
253
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
254
+ }
255
+ return (await response.json()) as T
256
+ } finally {
257
+ clearTimeout(timeoutId)
258
+ }
259
+ }
260
+
261
+ private _buildCustomPagesUrl(
262
+ projectId: string,
263
+ options: WeaverseNextFetchCustomPagesOptions,
264
+ cursor: string | null
265
+ ) {
266
+ let params = new URLSearchParams()
267
+ if (options.locale) {
268
+ params.set('locale', options.locale)
269
+ }
270
+ if (options.limit) {
271
+ params.set('limit', String(options.limit))
272
+ }
273
+ if (cursor) {
274
+ params.set('cursor', cursor)
275
+ }
276
+ let qs = params.toString()
277
+ return `${this._baseConfigs.weaverseApiBase}/api/public/v1/projects/${projectId}/custom-pages${
278
+ qs ? `?${qs}` : ''
279
+ }`
280
+ }
281
+
282
+ /**
283
+ * Fetch all published custom pages for sitemap generation. Mirrors Hydrogen's
284
+ * helper but uses Next's fetch cache knobs (`revalidate` / `tags`). It returns
285
+ * accumulated results on partial pagination failure so a single API hiccup
286
+ * does not break sitemap generation entirely.
287
+ */
288
+ fetchCustomPages = async (
289
+ options: WeaverseNextFetchCustomPagesOptions = {}
290
+ ): Promise<WeaverseNextCustomPageEntry[]> => {
291
+ let projectId = await this.resolveProjectId()
292
+ let entries: WeaverseNextCustomPageEntry[] = []
293
+ if (!projectId) {
294
+ console.warn(
295
+ '[WeaverseNext] Failed to fetch custom pages: missing projectId'
296
+ )
297
+ return entries
298
+ }
299
+
300
+ let cursor: string | null = null
301
+ for (let page = 0; page < MAX_CUSTOM_PAGE_PAGES; page += 1) {
302
+ let url = this._buildCustomPagesUrl(projectId, options, cursor)
303
+
304
+ try {
305
+ let result = await this.fetchWithCache<{
306
+ data?: WeaverseNextCustomPageEntry[]
307
+ nextCursor?: string | null
308
+ }>(url, {
309
+ revalidate: options.revalidate,
310
+ tags: options.tags,
311
+ })
312
+ entries.push(...(result.data ?? []))
313
+ if (!result.nextCursor) {
314
+ return entries
315
+ }
316
+ cursor = result.nextCursor
317
+ } catch (error) {
318
+ let message = error instanceof Error ? error.message : String(error)
319
+ console.warn(
320
+ entries.length === 0
321
+ ? `[WeaverseNext] Failed to fetch custom pages: ${message}`
322
+ : `[WeaverseNext] Custom pages pagination interrupted: ${message}`
323
+ )
324
+ return entries
325
+ }
326
+ }
327
+
328
+ console.warn('[WeaverseNext] Custom pages pagination cap reached')
329
+ return entries
330
+ }
331
+
332
+ private _generateFallbackPage(message: string): WeaverseNextPageData {
333
+ let rootId = generateUuid()
334
+ return {
335
+ id: `fallback_page_${rootId}`,
336
+ rootId,
337
+ name: 'Default Page',
338
+ items: [
339
+ {
340
+ type: 'main',
341
+ id: rootId,
342
+ data: { dangerouslySetInnerHTML: { __html: message } },
343
+ },
344
+ ],
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Recursively materialize a section preset (and its preset children) into a
350
+ * flat list of page items, returning the new item id. Ported from Hydrogen's
351
+ * `recursivelyAddDataItem`. Returns `undefined` when the type isn't registered.
352
+ */
353
+ private _buildPreviewItems(
354
+ type: string,
355
+ items: WeaverseNextComponentData[],
356
+ initData: Record<string, unknown> = {}
357
+ ): string | undefined {
358
+ let component = this.components.find((c) => c.schema?.type === type)
359
+ if (!component) {
360
+ return
361
+ }
362
+ let { children, ...data } = (component.schema?.presets ?? {}) as {
363
+ children?: { type: string; [key: string]: unknown }[]
364
+ [key: string]: unknown
365
+ }
366
+ let childIds: { id: string }[] = []
367
+ if (children) {
368
+ for (let child of children) {
369
+ let { type: childType, children: _children, ...childData } = child
370
+ let childId = this._buildPreviewItems(childType, items, childData)
371
+ if (childId) {
372
+ childIds.push({ id: childId })
373
+ }
374
+ }
375
+ }
376
+ let id = generateUuid()
377
+ items.push({ id, type, data: { ...data, ...initData }, children: childIds })
378
+ return id
379
+ }
380
+
381
+ /**
382
+ * Synthetic loader data for Studio "section preview" mode. Mirrors Hydrogen's
383
+ * `getPreviewData`: it builds a single-section page from the requested
384
+ * `sectionType`'s presets without hitting `/api/public/project`, so preview
385
+ * works even without a resolved `projectId`.
386
+ */
387
+ private _generatePreviewData(projectId: string): WeaverseNextLoaderData {
388
+ let { sectionType, weaverseHost } = this._baseConfigs
389
+ let items: WeaverseNextComponentData[] = []
390
+ let sectionId = sectionType
391
+ ? this._buildPreviewItems(sectionType, items)
392
+ : undefined
393
+
394
+ let configs = {
395
+ ...this._buildPublicConfigs(projectId),
396
+ isPreviewMode: true,
397
+ weaverseHost,
398
+ requestInfo: buildWeaverseNextRequestInfo(this.requestContext),
399
+ }
400
+
401
+ return {
402
+ project: {
403
+ id: projectId,
404
+ weaverseShopId: 'shop-id',
405
+ name: 'Section Preview',
406
+ },
407
+ configs,
408
+ page: {
409
+ id: 'weaverse-preview-page',
410
+ name: 'Preview section',
411
+ rootId: '1',
412
+ items: [
413
+ {
414
+ id: '1',
415
+ type: 'main',
416
+ data: {},
417
+ children: sectionId ? [{ id: sectionId }] : [],
418
+ },
419
+ ...items,
420
+ ],
421
+ },
422
+ }
423
+ }
424
+
425
+ private _getLoadPageInput(input: WeaverseNextLoadPageInput) {
426
+ let {
427
+ cache,
428
+ projectId: routeProjectId,
429
+ ...rest
430
+ } = input as WeaverseNextServerLoadPageInput
431
+ let params: Record<string, unknown> = {}
432
+ for (let [key, value] of Object.entries(rest)) {
433
+ if (value !== undefined) {
434
+ params[key] = value
435
+ }
436
+ }
437
+ return { cache, params, routeProjectId }
438
+ }
439
+
440
+ private async _resolveLoadPageProjectId(routeProjectId?: string) {
441
+ let effectiveProjectId =
442
+ (typeof routeProjectId === 'string' && routeProjectId.trim()) ||
443
+ (await this.resolveProjectId())
444
+ if (effectiveProjectId) {
445
+ this.configs = this._buildPublicConfigs(effectiveProjectId)
446
+ }
447
+ return effectiveProjectId
448
+ }
449
+
450
+ private _fetchPagePayload(
451
+ projectId: string,
452
+ params: Record<string, unknown>,
453
+ cache?: WeaverseNextCacheConfig
454
+ ) {
455
+ let i18n = this.requestContext?.i18n ?? this.commerce?.storefront?.i18n
456
+ let url = normalizeNextPageUrl(resolveRequestUrl(this.requestContext))
457
+ let apiUrl = `${this._baseConfigs.weaverseApiBase}/api/public/project`
458
+ let userAgent = this.requestContext?.headers?.get('user-agent') ?? ''
459
+ return this.fetchWithCache<Record<string, unknown>>(apiUrl, {
460
+ method: 'POST',
461
+ body: JSON.stringify({
462
+ projectId,
463
+ url,
464
+ i18n,
465
+ params,
466
+ isDesignMode: this._baseConfigs.isDesignMode,
467
+ }),
468
+ headers: { ...JSON_HEADERS, 'X-Visitor-UA': userAgent },
469
+ ...(cache ?? {}),
470
+ })
471
+ }
472
+
473
+ private _assertNoPageError(payload: Record<string, unknown>) {
474
+ if ('error' in payload && typeof payload.error === 'string') {
475
+ throw new Error(payload.error)
476
+ }
477
+ }
478
+
479
+ private _createLoaderData(
480
+ payload: Record<string, unknown>,
481
+ projectId: string
482
+ ): WeaverseNextLoaderData {
483
+ this._assertNoPageError(payload)
484
+ let page = payload.page as WeaverseNextPageData | undefined
485
+ let project = payload.project as WeaverseNextLoaderData['project']
486
+ let pageAssignment = normalizeWeaverseNextPageAssignment(
487
+ payload.pageAssignment
488
+ )
489
+
490
+ page ??= this._generateFallbackPage(
491
+ '<div style="text-align: center;">Please add new section to start.</div>'
492
+ )
493
+ project ??= {
494
+ id: projectId,
495
+ name: 'Weaverse project not found.',
496
+ weaverseShopId: '',
497
+ }
498
+ if (Array.isArray(page.items) && page.items.length === 0) {
499
+ page.items.push({
500
+ type: 'main',
501
+ id: page.rootId || generateUuid(),
502
+ data: {},
503
+ })
504
+ }
505
+
506
+ return {
507
+ page,
508
+ project,
509
+ pageAssignment,
510
+ configs: {
511
+ ...this._buildPublicConfigs(projectId),
512
+ requestInfo: buildWeaverseNextRequestInfo(this.requestContext),
513
+ },
514
+ }
515
+ }
516
+
517
+ private async _runLoaders(data: WeaverseNextLoaderData) {
518
+ this.data = data
519
+ let withLoaders = await runWeaverseComponentLoaders({ client: this, data })
520
+ this.data = withLoaders ?? data
521
+ return this.data
522
+ }
523
+
524
+ loadPage = async (
525
+ input: WeaverseNextLoadPageInput = {}
526
+ ): Promise<WeaverseNextLoaderData | null> => {
527
+ try {
528
+ let { cache, params, routeProjectId } = this._getLoadPageInput(input)
529
+ let projectId = await this._resolveLoadPageProjectId(routeProjectId)
530
+ if (this._baseConfigs.isPreviewMode) {
531
+ let previewData = this._generatePreviewData(projectId || 'x')
532
+ this.data = previewData
533
+ return previewData
534
+ }
535
+ if (!projectId) {
536
+ throw new Error('Missing Weaverse projectId!')
537
+ }
538
+
539
+ let payload = await this._fetchPagePayload(projectId, params, cache)
540
+ let data = this._createLoaderData(payload, projectId)
541
+ return await this._runLoaders(data)
542
+ } catch (error) {
543
+ console.error('❌ Page load failed', {
544
+ url: this.requestContext?.url,
545
+ projectId: input.projectId || this.configs.projectId,
546
+ message: error instanceof Error ? error.message : String(error),
547
+ })
548
+ return null
549
+ }
550
+ }
551
+
552
+ /**
553
+ * Resolve the request locale (e.g. `fr-ca`) used for merchant overrides.
554
+ * Unlike Hydrogen — which defaults to `en`/`us` because a Hydrogen
555
+ * storefront always has an i18n context — Next apps may not thread one
556
+ * through, so a missing language or country means "no explicit locale" and
557
+ * the translation fetch is skipped rather than guessed.
558
+ */
559
+ private _resolveOverridesLocale(): string | undefined {
560
+ let i18n = this.requestContext?.i18n
561
+ let language =
562
+ typeof i18n?.language === 'string' ? i18n.language.trim() : ''
563
+ let country = typeof i18n?.country === 'string' ? i18n.country.trim() : ''
564
+ if (!(language && country)) {
565
+ return
566
+ }
567
+ return `${language.toLowerCase()}-${country.toLowerCase()}`
568
+ }
569
+
570
+ /**
571
+ * Fetch locale-specific static-text overrides from the translation API.
572
+ * Ported from Hydrogen's `fetchMerchantOverrides`: skipped entirely when the
573
+ * theme has no `i18n` schema (nothing translatable) or when the request has
574
+ * no explicit locale, and never allowed to fail theme settings.
575
+ */
576
+ private async _fetchMerchantOverrides(
577
+ projectId: string,
578
+ options: WeaverseNextThemeSettingsOptions
579
+ ): Promise<Record<string, unknown> | undefined> {
580
+ let { weaverseHost } = this._baseConfigs
581
+ if (!(asThemeSchema(this.themeSchema)?.i18n && projectId && weaverseHost)) {
582
+ return
583
+ }
584
+
585
+ let locale = this._resolveOverridesLocale()
586
+ if (!locale) {
587
+ return
588
+ }
589
+
590
+ let url = `${weaverseHost}/api/translation/static?projectId=${projectId}&locale=${locale}`
591
+ try {
592
+ let overrides = await this.fetchWithCache<Record<string, unknown>>(url, {
593
+ method: 'GET',
594
+ headers: { Accept: 'application/json' },
595
+ revalidate: options.revalidate,
596
+ tags: options.tags,
597
+ })
598
+ return typeof overrides === 'object' && overrides !== null
599
+ ? overrides
600
+ : undefined
601
+ } catch (error) {
602
+ // Translations are additive — a failure must not break theme settings.
603
+ console.warn(
604
+ '[WeaverseNext] Unable to load merchant overrides, using theme defaults:',
605
+ error instanceof Error ? error.message : String(error)
606
+ )
607
+ return
608
+ }
609
+ }
610
+
611
+ loadThemeSettings = async (
612
+ // Broad param keeps this assignable to `WeaverseNextClient.loadThemeSettings`;
613
+ // only the cache fields below are honored — no request-context override.
614
+ optionsInput:
615
+ | WeaverseNextThemeSettingsOptions
616
+ | WeaverseNextRequestContext = {}
617
+ ): Promise<WeaverseNextThemeSettingsResponse> => {
618
+ let options = optionsInput as WeaverseNextThemeSettingsOptions
619
+ let themeSchema = asThemeSchema(this.themeSchema)
620
+ let defaultThemeSettings = generateDataFromSchema(this.themeSchema)
621
+ let staticContent = themeSchema?.i18n?.staticContent
622
+
623
+ try {
624
+ let projectId = await this.resolveProjectId()
625
+ if (!projectId) {
626
+ throw new Error('Missing Weaverse projectId!')
627
+ }
628
+
629
+ let { isDesignMode } = this._baseConfigs
630
+ let url = `${this._baseConfigs.weaverseApiBase}/api/public/project_configs`
631
+ // Project configs and locale overrides are independent reads — mirror
632
+ // Hydrogen and fetch them in parallel instead of serially.
633
+ let [result, merchantOverrides] = await Promise.all([
634
+ this.fetchWithCache<WeaverseNextThemeSettingsResponse>(url, {
635
+ method: 'POST',
636
+ body: JSON.stringify({ projectId, isDesignMode }),
637
+ headers: JSON_HEADERS,
638
+ revalidate: options.revalidate,
639
+ tags: options.tags,
640
+ }),
641
+ this._fetchMerchantOverrides(projectId, options),
642
+ ])
643
+
644
+ if (typeof result !== 'object' || result === null) {
645
+ result = { theme: defaultThemeSettings }
646
+ }
647
+
648
+ if (themeSchema?.settings || themeSchema?.inspector) {
649
+ if (!result.theme || typeof result.theme !== 'object') {
650
+ result.theme = defaultThemeSettings
651
+ } else {
652
+ result.theme = { ...defaultThemeSettings, ...result.theme }
653
+ }
654
+ }
655
+
656
+ if (staticContent) {
657
+ result.staticContent = staticContent
658
+ }
659
+
660
+ // Only overwrite when the translation API actually returned overrides so
661
+ // a missing/failed fetch can't wipe overrides shipped by project_configs.
662
+ if (merchantOverrides) {
663
+ result.merchantOverrides = merchantOverrides
664
+ }
665
+
666
+ if (isDesignMode) {
667
+ let serializableSchema: WeaverseNextThemeSchema | undefined =
668
+ themeSchema
669
+ ? {
670
+ ...themeSchema,
671
+ settings: themeSchema.settings?.map((group) => ({
672
+ ...group,
673
+ inputs: group?.inputs?.map((inputItem) =>
674
+ typeof inputItem?.condition === 'function'
675
+ ? {
676
+ ...inputItem,
677
+ condition: inputItem.condition.toString(),
678
+ }
679
+ : inputItem
680
+ ),
681
+ })),
682
+ }
683
+ : undefined
684
+ result = {
685
+ ...result,
686
+ schema: serializableSchema,
687
+ publicEnv: this._baseConfigs.publicEnv,
688
+ }
689
+ }
690
+
691
+ return result
692
+ } catch (error) {
693
+ let errorDetails = error instanceof Error ? error.message : String(error)
694
+ console.error('Unable to load theme settings', errorDetails)
695
+ return {
696
+ theme: defaultThemeSettings,
697
+ staticContent,
698
+ _error: errorDetails,
699
+ _loadFailed: true,
700
+ }
701
+ }
702
+ }
703
+ }
704
+
705
+ /**
706
+ * Create a server-side {@link WeaverseNextServerClient} for Next.js App Router
707
+ * routes / server components.
708
+ *
709
+ * @example
710
+ * ```ts
711
+ * let weaverse = createWeaverseNextServerClient({
712
+ * projectId: process.env.WEAVERSE_PROJECT_ID,
713
+ * components,
714
+ * commerce: { storefront },
715
+ * requestContext: { url, headers, searchParams, i18n },
716
+ * cache: { revalidate: 60, tags: ['weaverse'] },
717
+ * })
718
+ * let data = await weaverse.loadPage({ type: 'INDEX' })
719
+ * let theme = await weaverse.loadThemeSettings()
720
+ * ```
721
+ */
722
+ export function createWeaverseNextServerClient(
723
+ config: WeaverseNextServerClientConfig
724
+ ): WeaverseNextServerClient {
725
+ return new NextServerClient(config)
726
+ }
727
+
728
+ export { getContextSearchParams } from './configs'