@weaverse/next 0.1.0-alpha.13 → 0.1.0-alpha.15

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