@weaverse/next 0.1.0-alpha.2 → 0.1.0-alpha.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +586 -123
- package/dist/chunk-LH6AAEIK.mjs +326 -0
- package/dist/chunk-LH6AAEIK.mjs.map +1 -0
- package/dist/index.js +784 -60
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +588 -58
- package/dist/index.mjs.map +1 -1
- package/dist/server.js +537 -76
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +363 -27
- package/dist/server.mjs.map +1 -1
- package/dist/types/client.d.ts +16 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/hooks.d.ts +20 -0
- package/dist/types/hooks.d.ts.map +1 -0
- package/dist/types/index.d.ts +34 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/item.d.ts +60 -0
- package/dist/types/item.d.ts.map +1 -0
- package/dist/types/loader.d.ts +25 -0
- package/dist/types/loader.d.ts.map +1 -0
- package/dist/types/main.d.ts +11 -0
- package/dist/types/main.d.ts.map +1 -0
- package/dist/types/provider.d.ts +82 -0
- package/dist/types/provider.d.ts.map +1 -0
- package/dist/types/registry.d.ts +15 -0
- package/dist/types/registry.d.ts.map +1 -0
- package/dist/types/renderer.d.ts +21 -0
- package/dist/types/renderer.d.ts.map +1 -0
- package/dist/types/request-info.d.ts +19 -0
- package/dist/types/request-info.d.ts.map +1 -0
- package/dist/types/revalidate-item.d.ts +82 -0
- package/dist/types/revalidate-item.d.ts.map +1 -0
- package/dist/types/root-provider.d.ts +62 -0
- package/dist/types/root-provider.d.ts.map +1 -0
- package/dist/types/runtime.d.ts +113 -0
- package/dist/types/runtime.d.ts.map +1 -0
- package/dist/types/seo.d.ts +55 -0
- package/dist/types/seo.d.ts.map +1 -0
- package/dist/types/server/configs.d.ts +27 -0
- package/dist/types/server/configs.d.ts.map +1 -0
- package/dist/types/server/normalize-page-url.d.ts +19 -0
- package/dist/types/server/normalize-page-url.d.ts.map +1 -0
- package/dist/types/server/revalidate-handler.d.ts +55 -0
- package/dist/types/server/revalidate-handler.d.ts.map +1 -0
- package/dist/types/server/server-client.d.ts +21 -0
- package/dist/types/server/server-client.d.ts.map +1 -0
- package/dist/types/server.d.ts +19 -0
- package/dist/types/server.d.ts.map +1 -0
- package/dist/types/studio-bridge.d.ts +16 -0
- package/dist/types/studio-bridge.d.ts.map +1 -0
- package/dist/types/studio-connect.d.ts +27 -0
- package/dist/types/studio-connect.d.ts.map +1 -0
- package/dist/types/studio-router.d.ts +50 -0
- package/dist/types/studio-router.d.ts.map +1 -0
- package/dist/types/studio-script-src.d.ts +12 -0
- package/dist/types/studio-script-src.d.ts.map +1 -0
- package/dist/types/theme-settings-store.d.ts +18 -0
- package/dist/types/theme-settings-store.d.ts.map +1 -0
- package/dist/types/translation-context.d.ts +156 -0
- package/dist/types/translation-context.d.ts.map +1 -0
- package/dist/types/translation-store.d.ts +44 -0
- package/dist/types/translation-store.d.ts.map +1 -0
- package/dist/types/types.d.ts +665 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/use-weaverse-next-studio.d.ts +34 -0
- package/dist/types/use-weaverse-next-studio.d.ts.map +1 -0
- package/dist/types/utils.d.ts +12 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/package.json +12 -8
- package/src/client.ts +102 -0
- package/src/hooks.ts +50 -0
- package/src/index.ts +153 -0
- package/src/item.ts +167 -0
- package/src/loader.ts +135 -0
- package/src/main.tsx +45 -0
- package/src/provider.tsx +225 -0
- package/src/registry.ts +63 -0
- package/src/renderer.tsx +100 -0
- package/src/request-info.ts +124 -0
- package/src/revalidate-item.ts +211 -0
- package/src/root-provider.tsx +182 -0
- package/src/runtime.ts +555 -0
- package/src/seo.ts +157 -0
- package/src/server/configs.ts +178 -0
- package/src/server/normalize-page-url.ts +73 -0
- package/src/server/revalidate-handler.ts +357 -0
- package/src/server/server-client.ts +728 -0
- package/src/server.ts +67 -0
- package/src/studio-bridge.tsx +82 -0
- package/src/studio-connect.tsx +88 -0
- package/src/studio-router.ts +85 -0
- package/src/studio-script-src.ts +128 -0
- package/src/theme-settings-store.ts +47 -0
- package/src/translation-context.tsx +299 -0
- package/src/translation-store.ts +69 -0
- package/src/types.ts +753 -0
- package/src/use-weaverse-next-studio.tsx +106 -0
- package/src/utils.ts +37 -0
- package/dist/chunk-SS6QUKYY.mjs +0 -149
- package/dist/chunk-SS6QUKYY.mjs.map +0 -1
- package/dist/index.d.mts +0 -250
- package/dist/index.d.ts +0 -250
- package/dist/server.d.mts +0 -60
- package/dist/server.d.ts +0 -60
- package/dist/types-DvEeLX6L.d.mts +0 -321
- package/dist/types-DvEeLX6L.d.ts +0 -321
|
@@ -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'
|