@weaverse/hydrogen 5.23.0 → 6.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +29 -2271
- package/dist/index.mjs +4 -2213
- package/dist/internal.js +64 -0
- package/dist/internal.js.map +1 -0
- package/dist/internal.mjs +16 -0
- package/dist/internal.mjs.map +1 -0
- package/dist/next-server.js +14 -0
- package/dist/next-server.mjs +2 -0
- package/dist/next.js +119 -0
- package/dist/next.mjs +5 -0
- package/dist/react-router.js +68 -0
- package/dist/react-router.mjs +5 -0
- package/dist/react.js +26 -0
- package/dist/react.mjs +2 -0
- package/dist/translation-store-BpPWqZ0B.js +548 -0
- package/dist/translation-store-BpPWqZ0B.js.map +1 -0
- package/dist/translation-store-C42QdI0e.mjs +441 -0
- package/dist/translation-store-C42QdI0e.mjs.map +1 -0
- package/dist/types/WeaverseHydrogenRoot.d.ts +8 -8
- package/dist/types/WeaverseHydrogenRoot.d.ts.map +1 -1
- package/dist/types/components/index.d.ts +2 -2
- package/dist/types/components/main.d.ts +2 -2
- package/dist/types/hooks/translation-context.d.ts +2 -8
- package/dist/types/hooks/translation-context.d.ts.map +1 -1
- package/dist/types/index.d.ts +12 -33
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/internal.d.ts +24 -0
- package/dist/types/internal.d.ts.map +1 -0
- package/dist/types/next-server.d.ts +8 -0
- package/dist/types/next-server.d.ts.map +1 -0
- package/dist/types/next.d.ts +12 -0
- package/dist/types/next.d.ts.map +1 -0
- package/dist/types/react-router.d.ts +18 -0
- package/dist/types/react-router.d.ts.map +1 -0
- package/dist/types/react.d.ts +11 -0
- package/dist/types/react.d.ts.map +1 -0
- package/dist/types/seo.d.ts +4 -4
- package/dist/types/seo.d.ts.map +1 -1
- package/dist/types/types.d.ts +126 -67
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/utils/index.d.ts +4 -5
- package/dist/types/utils/index.d.ts.map +1 -1
- package/dist/types/utils/nested-key.d.ts +9 -0
- package/dist/types/utils/nested-key.d.ts.map +1 -0
- package/dist/types/utils/normalize-page-url.d.ts +1 -1
- package/dist/types/utils/product-options.d.ts +18 -0
- package/dist/types/utils/product-options.d.ts.map +1 -0
- package/dist/types/utils/translation-store.d.ts.map +1 -1
- package/dist/types/utils/use-studio.d.ts +1 -1
- package/dist/types/utils/use-studio.d.ts.map +1 -1
- package/dist/types/utils/use-theme-settings-store.d.ts +8 -8
- package/dist/types/utils/use-theme-settings-store.d.ts.map +1 -1
- package/dist/types/weaverse-client.d.ts +15 -16
- package/dist/types/weaverse-client.d.ts.map +1 -1
- package/dist/weaverse-client-Clu3u7md.mjs +1966 -0
- package/dist/weaverse-client-Clu3u7md.mjs.map +1 -0
- package/dist/weaverse-client-yC4TYuVV.js +2073 -0
- package/dist/weaverse-client-yC4TYuVV.js.map +1 -0
- package/package.json +44 -20
- package/readme.md +76 -23
- package/src/WeaverseHydrogenRoot.tsx +18 -15
- package/src/components/index.ts +2 -2
- package/src/components/main.tsx +2 -2
- package/src/hooks/translation-context.tsx +3 -28
- package/src/index.ts +42 -81
- package/src/internal.ts +64 -0
- package/src/next-server.ts +13 -0
- package/src/next.ts +35 -0
- package/src/react-router.ts +52 -0
- package/src/react.ts +16 -0
- package/src/seo.ts +5 -5
- package/src/types.ts +158 -73
- package/src/utils/index.ts +15 -14
- package/src/utils/nested-key.ts +28 -0
- package/src/utils/normalize-page-url.ts +1 -1
- package/src/utils/product-options.ts +28 -0
- package/src/utils/use-studio.ts +1 -1
- package/src/utils/use-theme-settings-store.ts +7 -7
- package/src/weaverse-client.ts +313 -148
- package/dist/index.js.map +0 -1
- package/dist/index.mjs.map +0 -1
package/src/weaverse-client.ts
CHANGED
|
@@ -1,32 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CacheCustom,
|
|
3
|
-
createWithCache,
|
|
4
|
-
type HydrogenEnv,
|
|
5
|
-
type I18nBase,
|
|
6
|
-
} from '@shopify/hydrogen'
|
|
1
|
+
import { Cache, createFetchWithCache } from '@shopify/hydrogen'
|
|
7
2
|
import type {
|
|
8
3
|
CachingStrategy,
|
|
9
4
|
CustomPageEntry,
|
|
10
5
|
FetchProjectPayload,
|
|
11
6
|
FetchProjectRequestBody,
|
|
12
|
-
HydrogenComponent,
|
|
13
|
-
HydrogenComponentData,
|
|
14
|
-
HydrogenPageData,
|
|
15
7
|
LoadPageParams,
|
|
16
8
|
PageType,
|
|
17
9
|
ProjectIdValidationResult,
|
|
18
|
-
ThemeSettingsResponse,
|
|
19
10
|
WeaverseClientArgs,
|
|
11
|
+
WeaverseComponent,
|
|
12
|
+
WeaverseComponentData,
|
|
13
|
+
WeaverseEnv,
|
|
14
|
+
WeaverseI18n,
|
|
20
15
|
WeaverseLoaderData,
|
|
16
|
+
WeaversePageData,
|
|
21
17
|
WeaverseProjectConfigs,
|
|
22
18
|
WeaverseStudioQueries,
|
|
19
|
+
WeaverseThemeSettingsResponse,
|
|
23
20
|
WithCacheFetchResponse,
|
|
24
21
|
} from './types'
|
|
25
22
|
import {
|
|
26
23
|
hasError,
|
|
27
|
-
isFetchProjectPayload,
|
|
28
24
|
isProjectIdFunction,
|
|
29
25
|
WeaverseError,
|
|
26
|
+
WeaverseLoadError,
|
|
27
|
+
type WeaverseLoadFailureKind,
|
|
30
28
|
} from './types'
|
|
31
29
|
import {
|
|
32
30
|
generateDataFromSchema,
|
|
@@ -41,14 +39,10 @@ import { normalizePageUrl } from './utils/normalize-page-url'
|
|
|
41
39
|
* These values optimize CDN hit rates while maintaining fresh content.
|
|
42
40
|
*/
|
|
43
41
|
const CACHE_DURATIONS = {
|
|
44
|
-
/**
|
|
42
|
+
/** Freshness for content that can change in Builder */
|
|
45
43
|
SHORT: 60,
|
|
46
|
-
/** Shared cache freshness for anonymous storefront traffic */
|
|
47
|
-
SHARED_SHORT: 300,
|
|
48
44
|
/** Long stale window so slow Builder responses do not block storefront rendering */
|
|
49
45
|
LONG: 86_400,
|
|
50
|
-
/** Extra freshness for sitemap-like index data */
|
|
51
|
-
STATIC: 3600,
|
|
52
46
|
} as const
|
|
53
47
|
|
|
54
48
|
/**
|
|
@@ -76,27 +70,160 @@ function isRetryableFetchError(error: unknown): boolean {
|
|
|
76
70
|
if (!(error instanceof WeaverseError)) {
|
|
77
71
|
return false
|
|
78
72
|
}
|
|
79
|
-
|
|
73
|
+
// A successful status with an unparsable body was classified as a network
|
|
74
|
+
// error before it got its own code; it keeps the same single retry.
|
|
75
|
+
if (
|
|
76
|
+
error.code === 'TIMEOUT_ERROR' ||
|
|
77
|
+
error.code === 'NETWORK_ERROR' ||
|
|
78
|
+
error.code === 'INVALID_RESPONSE'
|
|
79
|
+
) {
|
|
80
80
|
return true
|
|
81
81
|
}
|
|
82
82
|
return error.code === 'FETCH_ERROR' && (error.statusCode ?? 0) >= 500
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/** Parses a JSON body, or returns the raw text when it is not JSON. */
|
|
86
|
+
function parseResponseText(text: string): unknown {
|
|
87
|
+
if (!text) {
|
|
88
|
+
return text
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(text)
|
|
92
|
+
} catch {
|
|
93
|
+
return text
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
98
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Whether a successful `/project` body is a page envelope.
|
|
103
|
+
*
|
|
104
|
+
* `page: null` is Builder's authoritative "nothing assigned at this URL" and
|
|
105
|
+
* stays valid; anything without a `page` field, with an `error` field, or with
|
|
106
|
+
* a page that has no item list is not an answer about this URL at all.
|
|
107
|
+
*/
|
|
108
|
+
function isProjectEnvelope(payload: unknown): boolean {
|
|
109
|
+
if (!isPlainObject(payload) || hasError(payload) || !('page' in payload)) {
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
const { page, project } = payload
|
|
113
|
+
const validPage =
|
|
114
|
+
page === null || (isPlainObject(page) && Array.isArray(page.items))
|
|
115
|
+
return validPage && (project == null || isPlainObject(project))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Whether a successful `/project_configs` body carries theme settings.
|
|
120
|
+
*
|
|
121
|
+
* Builder always sends the `theme` field; `null` means the merchant never
|
|
122
|
+
* saved settings, so schema defaults are the real configuration.
|
|
123
|
+
*/
|
|
124
|
+
function isThemeConfigsEnvelope(payload: unknown): boolean {
|
|
125
|
+
return (
|
|
126
|
+
isPlainObject(payload) &&
|
|
127
|
+
!hasError(payload) &&
|
|
128
|
+
'theme' in payload &&
|
|
129
|
+
(payload.theme === null || isPlainObject(payload.theme))
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Classifies a page-load failure. `'absent'` is Builder's 404 for a project
|
|
135
|
+
* that does not exist, which keeps resolving to `null` like before.
|
|
136
|
+
*/
|
|
137
|
+
function classifyPageLoadFailure(
|
|
138
|
+
error: unknown
|
|
139
|
+
): WeaverseLoadFailureKind | 'absent' {
|
|
140
|
+
if (error instanceof WeaverseLoadError) {
|
|
141
|
+
return error.kind
|
|
142
|
+
}
|
|
143
|
+
if (!(error instanceof WeaverseError)) {
|
|
144
|
+
// Thrown while turning a response into page data: the body was not
|
|
145
|
+
// the shape it claimed to be.
|
|
146
|
+
return 'invalid-response'
|
|
147
|
+
}
|
|
148
|
+
if (error.code === 'TIMEOUT_ERROR' || error.code === 'NETWORK_ERROR') {
|
|
149
|
+
return 'unavailable'
|
|
150
|
+
}
|
|
151
|
+
if (error.code === 'INVALID_RESPONSE') {
|
|
152
|
+
return 'invalid-response'
|
|
153
|
+
}
|
|
154
|
+
if (error.code === 'FETCH_ERROR') {
|
|
155
|
+
const status = error.statusCode ?? 0
|
|
156
|
+
if (status === 404) {
|
|
157
|
+
return 'absent'
|
|
158
|
+
}
|
|
159
|
+
if (status === 408 || status === 425 || status === 429 || status >= 500) {
|
|
160
|
+
return 'unavailable'
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Refusals (400/401/403) and project-ID errors: retrying cannot succeed.
|
|
164
|
+
return 'configuration'
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Validators for Builder requests made by this client itself, keyed on the
|
|
169
|
+
* exact options object (same reasoning as `bodyFreeCacheRequests`). On the
|
|
170
|
+
* cached path such a request rejects a non-2xx status instead of resolving
|
|
171
|
+
* to `null`, and only stores a body its validator accepts. Every other
|
|
172
|
+
* `fetchWithCache` caller keeps the existing behavior.
|
|
173
|
+
*/
|
|
174
|
+
const builderPayloadValidators = new WeakMap<
|
|
175
|
+
object,
|
|
176
|
+
(payload: unknown) => boolean
|
|
177
|
+
>()
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Gives a rejection from Hydrogen's cached fetch the code `fetchOnce` gives
|
|
181
|
+
* the same failure on the direct path.
|
|
182
|
+
*
|
|
183
|
+
* Hydrogen calls the platform `fetch` itself, so a request that never got a
|
|
184
|
+
* response rejects with the platform's error instead of a `WeaverseError`.
|
|
185
|
+
* The Fetch standard rejects a network failure with a `TypeError` and an
|
|
186
|
+
* aborted request with the signal's `AbortError`/`TimeoutError`; those are
|
|
187
|
+
* outages. Anything else is returned unchanged, so it is not reported as one.
|
|
188
|
+
*/
|
|
189
|
+
function cachedTransportError(error: unknown, url: string): unknown {
|
|
190
|
+
if (error instanceof WeaverseError || !(error instanceof Error)) {
|
|
191
|
+
return error
|
|
192
|
+
}
|
|
193
|
+
if (error.name === 'AbortError' || error.name === 'TimeoutError') {
|
|
194
|
+
return new WeaverseError('Request timed out', 'TIMEOUT_ERROR', undefined, {
|
|
195
|
+
url,
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
if (error instanceof TypeError) {
|
|
199
|
+
return new WeaverseError(
|
|
200
|
+
'Network error during fetch',
|
|
201
|
+
'NETWORK_ERROR',
|
|
202
|
+
undefined,
|
|
203
|
+
{ url, cause: error.message }
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
return error
|
|
207
|
+
}
|
|
208
|
+
|
|
85
209
|
/**
|
|
86
210
|
* Weaverse API version path
|
|
87
211
|
*/
|
|
88
212
|
const API_PATH = 'v1' as const
|
|
89
213
|
|
|
90
214
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
215
|
+
* Cache strategy for Weaverse API requests: fresh for a minute, then served
|
|
216
|
+
* stale for a day while it refreshes or while Builder errors.
|
|
217
|
+
*
|
|
218
|
+
* Every Builder resource uses it. Hydrogen 2026.4's subrequest cache only read
|
|
219
|
+
* `max-age` for freshness, so the old per-resource `sMaxAge` values never
|
|
220
|
+
* changed how long an entry stayed fresh.
|
|
93
221
|
*/
|
|
94
|
-
const DEFAULT_CACHE_STRATEGY: CachingStrategy = {
|
|
222
|
+
const DEFAULT_CACHE_STRATEGY: CachingStrategy = Cache({
|
|
95
223
|
maxAge: CACHE_DURATIONS.SHORT,
|
|
96
|
-
sMaxAge: CACHE_DURATIONS.SHARED_SHORT,
|
|
97
224
|
staleWhileRevalidate: CACHE_DURATIONS.LONG,
|
|
98
225
|
staleIfError: CACHE_DURATIONS.LONG,
|
|
99
|
-
}
|
|
226
|
+
})
|
|
100
227
|
|
|
101
228
|
/** Builder API resource whose freshness policy should be used for a request. */
|
|
102
229
|
export type BuilderApiCacheTarget =
|
|
@@ -107,9 +234,9 @@ export type BuilderApiCacheTarget =
|
|
|
107
234
|
|
|
108
235
|
/** Fetch options accepted by {@link WeaverseClient.fetchWithCache}. */
|
|
109
236
|
export interface WeaverseFetchWithCacheOptions extends RequestInit {
|
|
110
|
-
/** Builder API resource
|
|
237
|
+
/** Builder API resource the request reads; part of the cache key. */
|
|
111
238
|
cacheTarget?: BuilderApiCacheTarget
|
|
112
|
-
/** Explicit Hydrogen caching strategy overriding the
|
|
239
|
+
/** Explicit Hydrogen caching strategy overriding the default. */
|
|
113
240
|
strategy?: CachingStrategy
|
|
114
241
|
}
|
|
115
242
|
|
|
@@ -121,18 +248,6 @@ export interface FetchCustomPagesOptions {
|
|
|
121
248
|
locale?: string
|
|
122
249
|
}
|
|
123
250
|
|
|
124
|
-
const SMART_CACHE_STRATEGIES: Record<BuilderApiCacheTarget, CachingStrategy> = {
|
|
125
|
-
project: DEFAULT_CACHE_STRATEGY,
|
|
126
|
-
'theme-settings': DEFAULT_CACHE_STRATEGY,
|
|
127
|
-
'merchant-overrides': DEFAULT_CACHE_STRATEGY,
|
|
128
|
-
'custom-pages': {
|
|
129
|
-
maxAge: CACHE_DURATIONS.SHORT,
|
|
130
|
-
sMaxAge: CACHE_DURATIONS.STATIC,
|
|
131
|
-
staleWhileRevalidate: CACHE_DURATIONS.LONG,
|
|
132
|
-
staleIfError: CACHE_DURATIONS.LONG,
|
|
133
|
-
},
|
|
134
|
-
} as const
|
|
135
|
-
|
|
136
251
|
/**
|
|
137
252
|
* Options objects created by the internal `project_configs` request whose
|
|
138
253
|
* subrequest cache identity deliberately excludes the body. Theme settings
|
|
@@ -176,12 +291,12 @@ export class WeaverseClient {
|
|
|
176
291
|
basePageRequestBody: Omit<FetchProjectRequestBody, 'url'>
|
|
177
292
|
/** Resolved project and rendering-mode configuration. */
|
|
178
293
|
configs: WeaverseProjectConfigs
|
|
179
|
-
/** Hydrogen
|
|
180
|
-
|
|
294
|
+
/** Hydrogen cached fetch used for Builder API requests. */
|
|
295
|
+
private cachedFetch: ReturnType<typeof createFetchWithCache>
|
|
181
296
|
|
|
182
297
|
/** Incoming request associated with this client instance. */
|
|
183
298
|
request: WeaverseClientArgs['request']
|
|
184
|
-
/**
|
|
299
|
+
/** Customer Account API client, when the storefront passes one. */
|
|
185
300
|
customerAccount: WeaverseClientArgs['customerAccount']
|
|
186
301
|
/** Hydrogen Storefront API client and locale context. */
|
|
187
302
|
storefront: WeaverseClientArgs['storefront']
|
|
@@ -189,21 +304,22 @@ export class WeaverseClient {
|
|
|
189
304
|
components: WeaverseClientArgs['components']
|
|
190
305
|
/** Theme schema used for defaults and Studio editing. */
|
|
191
306
|
themeSchema: WeaverseClientArgs['themeSchema']
|
|
192
|
-
/**
|
|
307
|
+
/** Runtime environment variables. */
|
|
193
308
|
env: WeaverseClientArgs['env']
|
|
194
309
|
/** Cache storage supplied by the Hydrogen runtime. */
|
|
195
310
|
cache: WeaverseClientArgs['cache']
|
|
196
|
-
/** Background execution hook supplied by the
|
|
311
|
+
/** Background execution hook supplied by the runtime. */
|
|
197
312
|
waitUntil: WeaverseClientArgs['waitUntil']
|
|
198
313
|
|
|
199
314
|
// Performance optimizations
|
|
200
315
|
private readonly parsedUrl: URL
|
|
201
|
-
private readonly componentsByType: Map<string,
|
|
316
|
+
private readonly componentsByType: Map<string, WeaverseComponent>
|
|
202
317
|
|
|
203
318
|
// Configuration
|
|
204
319
|
private readonly fetchTimeoutMs: number
|
|
320
|
+
private readonly throwPageLoadFailures: boolean
|
|
205
321
|
|
|
206
|
-
/** Creates a request-scoped client from the
|
|
322
|
+
/** Creates a request-scoped client from the storefront's request context. */
|
|
207
323
|
constructor(args: WeaverseClientArgs) {
|
|
208
324
|
// Assign required dependencies
|
|
209
325
|
this.request = args.request
|
|
@@ -221,6 +337,7 @@ export class WeaverseClient {
|
|
|
221
337
|
|
|
222
338
|
// Configuration
|
|
223
339
|
this.fetchTimeoutMs = args.fetchTimeoutMs || DEFAULT_FETCH_TIMEOUT_MS
|
|
340
|
+
this.throwPageLoadFailures = args.throwPageLoadFailures === true
|
|
224
341
|
|
|
225
342
|
// Initialize cache and configs
|
|
226
343
|
this.initializeCache(args)
|
|
@@ -238,10 +355,9 @@ export class WeaverseClient {
|
|
|
238
355
|
)
|
|
239
356
|
}
|
|
240
357
|
|
|
241
|
-
this.
|
|
358
|
+
this.cachedFetch = createFetchWithCache({
|
|
242
359
|
cache: args.cache,
|
|
243
|
-
waitUntil: args.waitUntil
|
|
244
|
-
request: args.request,
|
|
360
|
+
waitUntil: args.waitUntil,
|
|
245
361
|
})
|
|
246
362
|
}
|
|
247
363
|
|
|
@@ -251,16 +367,13 @@ export class WeaverseClient {
|
|
|
251
367
|
*/
|
|
252
368
|
private initializeConfigs(args: WeaverseClientArgs): void {
|
|
253
369
|
// Get base configs once to avoid duplicate calls
|
|
254
|
-
const baseConfigs = getWeaverseConfigs(
|
|
255
|
-
args.request,
|
|
256
|
-
args.env as HydrogenEnv
|
|
257
|
-
)
|
|
370
|
+
const baseConfigs = getWeaverseConfigs(args.request, args.env)
|
|
258
371
|
|
|
259
372
|
// Resolve projectId with multi-project support
|
|
260
373
|
const resolvedProjectId = this.resolveProjectIdSync(
|
|
261
374
|
args.projectId,
|
|
262
375
|
args.request,
|
|
263
|
-
args.env
|
|
376
|
+
args.env,
|
|
264
377
|
baseConfigs
|
|
265
378
|
)
|
|
266
379
|
|
|
@@ -308,8 +421,8 @@ export class WeaverseClient {
|
|
|
308
421
|
* Significantly improves performance for large component lists.
|
|
309
422
|
*/
|
|
310
423
|
private buildComponentLookupMap(
|
|
311
|
-
components:
|
|
312
|
-
): Map<string,
|
|
424
|
+
components: WeaverseComponent[]
|
|
425
|
+
): Map<string, WeaverseComponent> {
|
|
313
426
|
return new Map(components.map((comp) => [comp.schema?.type, comp]))
|
|
314
427
|
}
|
|
315
428
|
|
|
@@ -407,7 +520,7 @@ export class WeaverseClient {
|
|
|
407
520
|
private resolveProjectIdSync(
|
|
408
521
|
projectId: string | (() => string) | (() => Promise<string>) | undefined,
|
|
409
522
|
request: Request,
|
|
410
|
-
env:
|
|
523
|
+
env: WeaverseEnv,
|
|
411
524
|
baseConfigs?: WeaverseProjectConfigs
|
|
412
525
|
): string {
|
|
413
526
|
return (
|
|
@@ -525,7 +638,7 @@ export class WeaverseClient {
|
|
|
525
638
|
private resolveFromEnvironment(
|
|
526
639
|
baseConfigs: WeaverseProjectConfigs | undefined,
|
|
527
640
|
request: Request,
|
|
528
|
-
env:
|
|
641
|
+
env: WeaverseEnv
|
|
529
642
|
): string | null {
|
|
530
643
|
const configs = baseConfigs || getWeaverseConfigs(request, env)
|
|
531
644
|
|
|
@@ -538,7 +651,6 @@ export class WeaverseClient {
|
|
|
538
651
|
|
|
539
652
|
/**
|
|
540
653
|
* Helper method for direct fetching in design mode.
|
|
541
|
-
* Returns same format as withCache.fetch for consistency.
|
|
542
654
|
* Includes 10-second timeout to prevent hanging requests.
|
|
543
655
|
*/
|
|
544
656
|
private readonly fetchOnce = async <T>(
|
|
@@ -568,7 +680,22 @@ export class WeaverseClient {
|
|
|
568
680
|
)
|
|
569
681
|
}
|
|
570
682
|
|
|
571
|
-
|
|
683
|
+
let data: T
|
|
684
|
+
try {
|
|
685
|
+
data = (await response.json()) as T
|
|
686
|
+
} catch (parseError) {
|
|
687
|
+
// A cut-short body still times out; anything else is a successful
|
|
688
|
+
// status whose body is not JSON, which is not a network failure.
|
|
689
|
+
if (parseError?.name === 'AbortError') {
|
|
690
|
+
throw parseError
|
|
691
|
+
}
|
|
692
|
+
throw new WeaverseError(
|
|
693
|
+
'Response body is not valid JSON',
|
|
694
|
+
'INVALID_RESPONSE',
|
|
695
|
+
response.status,
|
|
696
|
+
{ url }
|
|
697
|
+
)
|
|
698
|
+
}
|
|
572
699
|
return { data, response }
|
|
573
700
|
} catch (err) {
|
|
574
701
|
if (err instanceof WeaverseError) {
|
|
@@ -635,7 +762,7 @@ export class WeaverseClient {
|
|
|
635
762
|
|
|
636
763
|
/**
|
|
637
764
|
* Fetch data with caching support for both design mode and production.
|
|
638
|
-
* Uses direct fetch in design mode, Hydrogen's
|
|
765
|
+
* Uses direct fetch in design mode, Hydrogen's cached fetch in production.
|
|
639
766
|
* Automatically includes projectId in cache key for multi-project isolation.
|
|
640
767
|
*
|
|
641
768
|
* @param url - The URL to fetch from
|
|
@@ -655,13 +782,8 @@ export class WeaverseClient {
|
|
|
655
782
|
options: WeaverseFetchWithCacheOptions = {}
|
|
656
783
|
): Promise<T> => {
|
|
657
784
|
const { cacheTarget, strategy: strategyOverride, ...fetchOptions } = options
|
|
658
|
-
const strategy =
|
|
659
|
-
|
|
660
|
-
CacheCustom(
|
|
661
|
-
cacheTarget
|
|
662
|
-
? SMART_CACHE_STRATEGIES[cacheTarget]
|
|
663
|
-
: DEFAULT_CACHE_STRATEGY
|
|
664
|
-
)
|
|
785
|
+
const strategy = strategyOverride || DEFAULT_CACHE_STRATEGY
|
|
786
|
+
const { body } = fetchOptions
|
|
665
787
|
|
|
666
788
|
// Update cache key to include method, body content, and projectId for multi-project isolation
|
|
667
789
|
// Prefix for easier debugging in cache systems
|
|
@@ -676,7 +798,9 @@ export class WeaverseClient {
|
|
|
676
798
|
options.method || 'GET',
|
|
677
799
|
bodyFreeCacheRequests.has(options)
|
|
678
800
|
? BODY_FREE_CACHE_SENTINEL
|
|
679
|
-
:
|
|
801
|
+
: body == null
|
|
802
|
+
? null
|
|
803
|
+
: String(body),
|
|
680
804
|
// WeakSet-derived discriminator: this slot cannot be produced by any
|
|
681
805
|
// caller-supplied shape, so no external call can reach the marked
|
|
682
806
|
// identity — even one that happens to send the sentinel string as its
|
|
@@ -685,40 +809,66 @@ export class WeaverseClient {
|
|
|
685
809
|
this.configs.projectId,
|
|
686
810
|
cacheTarget || 'default',
|
|
687
811
|
]
|
|
688
|
-
let result: WithCacheFetchResponse<T>
|
|
689
812
|
|
|
690
813
|
// Bypass the shared Hydrogen subrequest cache for design/revision modes
|
|
691
814
|
// and for the Cloudflare public API proxy. The proxy owns freshness with
|
|
692
815
|
// versioned cache keys; keeping Hydrogen's URL/body cache in front would
|
|
693
816
|
// keep serving a stale response even after Builder bumps the proxy's
|
|
694
817
|
// project version.
|
|
818
|
+
const validateBuilderPayload = builderPayloadValidators.get(options)
|
|
819
|
+
|
|
695
820
|
if (
|
|
696
821
|
this.configs.isDesignMode ||
|
|
697
822
|
this.configs.isRevisionPreview ||
|
|
698
823
|
this.isExternalPublicApiUrl(url)
|
|
699
824
|
) {
|
|
700
|
-
result = await this.directFetch<T>(url, fetchOptions)
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
825
|
+
const result = await this.directFetch<T>(url, fetchOptions)
|
|
826
|
+
return result.data
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
let response: Response
|
|
830
|
+
let text: string
|
|
831
|
+
try {
|
|
832
|
+
response = await this.cachedFetch(url, fetchOptions, {
|
|
833
|
+
key: cacheKey,
|
|
834
|
+
strategy,
|
|
835
|
+
shouldCacheResponse: async ({ text: readText }) => {
|
|
707
836
|
// An aborted request never caches: Hydrogen turns a body read cut
|
|
708
|
-
// short by the abort into an empty
|
|
837
|
+
// short by the abort into an empty body, not an error.
|
|
709
838
|
if (fetchOptions.signal?.aborted) {
|
|
710
839
|
return false
|
|
711
840
|
}
|
|
841
|
+
const payload = parseResponseText(await readText())
|
|
842
|
+
if (validateBuilderPayload && !validateBuilderPayload(payload)) {
|
|
843
|
+
return false
|
|
844
|
+
}
|
|
712
845
|
// Cache any non-error response (for both Weaverse API and third-party APIs)
|
|
713
|
-
return (
|
|
714
|
-
!hasError(response) && response !== null && response !== undefined
|
|
715
|
-
)
|
|
846
|
+
return !hasError(payload) && payload !== null && payload !== ''
|
|
716
847
|
},
|
|
717
|
-
|
|
718
|
-
|
|
848
|
+
})
|
|
849
|
+
text = await response.text()
|
|
850
|
+
} catch (error) {
|
|
851
|
+
// Only this client's own Builder requests are normalized; a
|
|
852
|
+
// third-party `fetchWithCache` caller keeps the platform error.
|
|
853
|
+
throw validateBuilderPayload ? cachedTransportError(error, url) : error
|
|
719
854
|
}
|
|
720
855
|
|
|
721
|
-
|
|
856
|
+
if (!response.ok) {
|
|
857
|
+
// For this client's own Builder requests a non-2xx status must not read
|
|
858
|
+
// as "nothing published"; surface the status the direct path already
|
|
859
|
+
// throws instead. Other callers keep the `null` body they always got.
|
|
860
|
+
if (validateBuilderPayload) {
|
|
861
|
+
throw new WeaverseError(
|
|
862
|
+
`HTTP ${response.status}: ${response.statusText}`,
|
|
863
|
+
'FETCH_ERROR',
|
|
864
|
+
response.status,
|
|
865
|
+
{ url }
|
|
866
|
+
)
|
|
867
|
+
}
|
|
868
|
+
return null as T
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
return parseResponseText(text) as T
|
|
722
872
|
}
|
|
723
873
|
|
|
724
874
|
/**
|
|
@@ -750,7 +900,7 @@ export class WeaverseClient {
|
|
|
750
900
|
*/
|
|
751
901
|
loadThemeSettings = async (
|
|
752
902
|
strategy?: CachingStrategy
|
|
753
|
-
): Promise<
|
|
903
|
+
): Promise<WeaverseThemeSettingsResponse> => {
|
|
754
904
|
const defaultThemeSettings = generateDataFromSchema(this.themeSchema)
|
|
755
905
|
try {
|
|
756
906
|
const { configs } = this
|
|
@@ -793,32 +943,28 @@ export class WeaverseClient {
|
|
|
793
943
|
},
|
|
794
944
|
}
|
|
795
945
|
bodyFreeCacheRequests.add(themeSettingsOptions)
|
|
946
|
+
builderPayloadValidators.set(themeSettingsOptions, isThemeConfigsEnvelope)
|
|
796
947
|
|
|
797
948
|
const [data, merchantOverrides] = await Promise.all([
|
|
798
|
-
this.fetchWithCache<
|
|
949
|
+
this.fetchWithCache<WeaverseThemeSettingsResponse>(
|
|
950
|
+
url,
|
|
951
|
+
themeSettingsOptions
|
|
952
|
+
),
|
|
799
953
|
this.fetchMerchantOverrides(strategy),
|
|
800
954
|
])
|
|
801
955
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
//
|
|
805
|
-
if (
|
|
806
|
-
|
|
807
|
-
'Invalid theme settings response format
|
|
808
|
-
|
|
809
|
-
type: typeof result,
|
|
810
|
-
isString: typeof result === 'string',
|
|
811
|
-
length:
|
|
812
|
-
typeof result === 'string' ? (result as string).length : 'N/A',
|
|
813
|
-
preview:
|
|
814
|
-
typeof result === 'string'
|
|
815
|
-
? `${(result as string).slice(0, 50)}...`
|
|
816
|
-
: String(result),
|
|
817
|
-
}
|
|
956
|
+
// A body without a `theme` field (corrupted content, an error body, an
|
|
957
|
+
// empty object) is not the merchant's configuration. Report it like any
|
|
958
|
+
// other failure so schema defaults are never presented as loaded.
|
|
959
|
+
if (!isThemeConfigsEnvelope(data)) {
|
|
960
|
+
throw new WeaverseError(
|
|
961
|
+
'Invalid theme settings response format',
|
|
962
|
+
'INVALID_RESPONSE'
|
|
818
963
|
)
|
|
819
|
-
result = { theme: defaultThemeSettings }
|
|
820
964
|
}
|
|
821
965
|
|
|
966
|
+
let result = data
|
|
967
|
+
|
|
822
968
|
if (this.themeSchema?.settings || this.themeSchema?.inspector) {
|
|
823
969
|
// Ensure result.theme exists and is an object before merging
|
|
824
970
|
if (!result.theme || typeof result.theme !== 'object') {
|
|
@@ -954,9 +1100,9 @@ export class WeaverseClient {
|
|
|
954
1100
|
* Used internally when API returns no page or when page needs default content.
|
|
955
1101
|
*
|
|
956
1102
|
* @param message - Custom message to display in the fallback page
|
|
957
|
-
* @returns
|
|
1103
|
+
* @returns WeaversePageData with single main component containing the message
|
|
958
1104
|
*/
|
|
959
|
-
generateFallbackPage = (message: string):
|
|
1105
|
+
generateFallbackPage = (message: string): WeaversePageData => {
|
|
960
1106
|
const rootId = crypto.randomUUID()
|
|
961
1107
|
return {
|
|
962
1108
|
id: `fallback_page_${Date.now()}`,
|
|
@@ -1060,6 +1206,24 @@ export class WeaverseClient {
|
|
|
1060
1206
|
loadPage = async (
|
|
1061
1207
|
params: LoadPageParams = {}
|
|
1062
1208
|
): Promise<WeaverseLoaderData | null> => {
|
|
1209
|
+
// Validate route-level projectId if provided
|
|
1210
|
+
if (params.projectId !== undefined) {
|
|
1211
|
+
const message =
|
|
1212
|
+
typeof params.projectId === 'string'
|
|
1213
|
+
? this.validateProjectId(params.projectId).error
|
|
1214
|
+
: `must be a string. Received: ${typeof params.projectId}`
|
|
1215
|
+
if (message) {
|
|
1216
|
+
console.error(`Route-level projectId validation failed: ${message}`)
|
|
1217
|
+
if (this.throwPageLoadFailures) {
|
|
1218
|
+
throw new WeaverseLoadError(
|
|
1219
|
+
'Route-level projectId validation failed',
|
|
1220
|
+
'configuration'
|
|
1221
|
+
)
|
|
1222
|
+
}
|
|
1223
|
+
return null
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1063
1227
|
try {
|
|
1064
1228
|
const { request, storefront, basePageRequestBody, basePageConfigs } = this
|
|
1065
1229
|
const {
|
|
@@ -1069,25 +1233,6 @@ export class WeaverseClient {
|
|
|
1069
1233
|
sectionType,
|
|
1070
1234
|
} = this.configs
|
|
1071
1235
|
|
|
1072
|
-
// Validate route-level projectId if provided
|
|
1073
|
-
if (params.projectId !== undefined) {
|
|
1074
|
-
if (typeof params.projectId !== 'string') {
|
|
1075
|
-
console.error(
|
|
1076
|
-
'Route-level projectId validation failed: must be a string. ' +
|
|
1077
|
-
`Received: ${typeof params.projectId}`
|
|
1078
|
-
)
|
|
1079
|
-
return null
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
const validation = this.validateProjectId(params.projectId)
|
|
1083
|
-
if (!validation.valid) {
|
|
1084
|
-
console.error(
|
|
1085
|
-
`Route-level projectId validation failed: ${validation.error}`
|
|
1086
|
-
)
|
|
1087
|
-
return null
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
1236
|
// Override client-level projectId with route-level projectId if provided
|
|
1092
1237
|
const effectiveProjectId = params.projectId || clientProjectId
|
|
1093
1238
|
|
|
@@ -1096,14 +1241,17 @@ export class WeaverseClient {
|
|
|
1096
1241
|
}
|
|
1097
1242
|
|
|
1098
1243
|
if (!effectiveProjectId) {
|
|
1099
|
-
throw new
|
|
1244
|
+
throw new WeaverseLoadError(
|
|
1245
|
+
'Missing Weaverse projectId!',
|
|
1246
|
+
'configuration'
|
|
1247
|
+
)
|
|
1100
1248
|
}
|
|
1101
1249
|
|
|
1102
1250
|
const { strategy, ...pageLoadParams } = params
|
|
1103
1251
|
const body: {
|
|
1104
1252
|
projectId: string
|
|
1105
1253
|
url: string
|
|
1106
|
-
i18n?:
|
|
1254
|
+
i18n?: WeaverseI18n
|
|
1107
1255
|
params?: {
|
|
1108
1256
|
type?: PageType
|
|
1109
1257
|
locale?: string
|
|
@@ -1131,40 +1279,45 @@ export class WeaverseClient {
|
|
|
1131
1279
|
}
|
|
1132
1280
|
const url = this.getApiUrl('project')
|
|
1133
1281
|
|
|
1282
|
+
const projectOptions: WeaverseFetchWithCacheOptions = {
|
|
1283
|
+
...reqInit,
|
|
1284
|
+
cacheTarget: 'project',
|
|
1285
|
+
strategy,
|
|
1286
|
+
}
|
|
1287
|
+
builderPayloadValidators.set(projectOptions, isProjectEnvelope)
|
|
1134
1288
|
const projectData: FetchProjectPayload =
|
|
1135
|
-
await this.fetchWithCache<FetchProjectPayload>(url,
|
|
1136
|
-
...reqInit,
|
|
1137
|
-
cacheTarget: 'project',
|
|
1138
|
-
strategy,
|
|
1139
|
-
})
|
|
1289
|
+
await this.fetchWithCache<FetchProjectPayload>(url, projectOptions)
|
|
1140
1290
|
|
|
1141
1291
|
if (hasError(projectData)) {
|
|
1142
|
-
throw new
|
|
1292
|
+
throw new WeaverseLoadError(
|
|
1293
|
+
'Weaverse API returned an error body',
|
|
1294
|
+
'invalid-response'
|
|
1295
|
+
)
|
|
1143
1296
|
}
|
|
1144
1297
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
project = projectData.project
|
|
1155
|
-
pageAssignment = projectData.pageAssignment
|
|
1156
|
-
} else {
|
|
1298
|
+
if (!isProjectEnvelope(projectData)) {
|
|
1299
|
+
// A body that does not answer for this URL is not "nothing assigned
|
|
1300
|
+
// here". Without the opt-in, keep the historical fallback page.
|
|
1301
|
+
if (this.throwPageLoadFailures) {
|
|
1302
|
+
throw new WeaverseLoadError(
|
|
1303
|
+
'Invalid project data structure received from API',
|
|
1304
|
+
'invalid-response'
|
|
1305
|
+
)
|
|
1306
|
+
}
|
|
1157
1307
|
console.warn(
|
|
1158
1308
|
'Invalid project data structure received from API, using fallback data',
|
|
1159
1309
|
{ projectId: effectiveProjectId }
|
|
1160
1310
|
)
|
|
1161
|
-
// Try to extract what we can from the malformed response
|
|
1162
|
-
const responseData = projectData as any
|
|
1163
|
-
page = responseData?.page
|
|
1164
|
-
project = responseData?.project
|
|
1165
|
-
pageAssignment = responseData?.pageAssignment
|
|
1166
1311
|
}
|
|
1167
1312
|
|
|
1313
|
+
// Extract what we can; a legacy-mode malformed body may lack any field.
|
|
1314
|
+
const responseData = projectData as Partial<FetchProjectPayload> | null
|
|
1315
|
+
let page: WeaversePageData | undefined = responseData?.page ?? undefined
|
|
1316
|
+
let project: WeaverseLoaderData['project'] | undefined =
|
|
1317
|
+
responseData?.project
|
|
1318
|
+
const pageAssignment: WeaverseLoaderData['pageAssignment'] | undefined =
|
|
1319
|
+
responseData?.pageAssignment
|
|
1320
|
+
|
|
1168
1321
|
if (!project) {
|
|
1169
1322
|
console.warn('Weaverse project not found. Id:', effectiveProjectId)
|
|
1170
1323
|
project = {
|
|
@@ -1207,6 +1360,7 @@ export class WeaverseClient {
|
|
|
1207
1360
|
},
|
|
1208
1361
|
}
|
|
1209
1362
|
} catch (error) {
|
|
1363
|
+
const failure = classifyPageLoadFailure(error)
|
|
1210
1364
|
const errorMessage = '❌ Page load failed'
|
|
1211
1365
|
const errorContext: Record<string, unknown> = {
|
|
1212
1366
|
projectId: params.projectId || this.configs.projectId,
|
|
@@ -1232,12 +1386,23 @@ export class WeaverseClient {
|
|
|
1232
1386
|
})
|
|
1233
1387
|
}
|
|
1234
1388
|
|
|
1235
|
-
|
|
1389
|
+
if (failure === 'absent' || !this.throwPageLoadFailures) {
|
|
1390
|
+
return null
|
|
1391
|
+
}
|
|
1392
|
+
if (error instanceof WeaverseLoadError) {
|
|
1393
|
+
throw error
|
|
1394
|
+
}
|
|
1395
|
+
// Fixed message: the cause can carry request-derived detail.
|
|
1396
|
+
throw new WeaverseLoadError(
|
|
1397
|
+
'Unable to load Weaverse page',
|
|
1398
|
+
failure,
|
|
1399
|
+
error instanceof WeaverseError ? error.statusCode : undefined
|
|
1400
|
+
)
|
|
1236
1401
|
}
|
|
1237
1402
|
}
|
|
1238
1403
|
|
|
1239
1404
|
/** Executes the registered loader for one component and attaches its result. */
|
|
1240
|
-
execComponentLoader = async (item:
|
|
1405
|
+
execComponentLoader = async (item: WeaverseComponentData) => {
|
|
1241
1406
|
const { data = {}, type, id } = item
|
|
1242
1407
|
const component = this.componentsByType.get(type)
|
|
1243
1408
|
const loader = component?.loader
|