@weaverse/hydrogen 5.17.1 → 5.18.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 +83 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +83 -1
- package/dist/index.mjs.map +1 -1
- package/dist/types/WeaverseHydrogenRoot.d.ts +218 -0
- package/dist/types/WeaverseHydrogenRoot.d.ts.map +1 -0
- package/dist/types/components/index.d.ts +4 -0
- package/dist/types/components/index.d.ts.map +1 -0
- package/dist/types/components/main.d.ts +7 -0
- package/dist/types/components/main.d.ts.map +1 -0
- package/dist/types/hooks/theme-text-context.d.ts +2 -0
- package/dist/types/hooks/theme-text-context.d.ts.map +1 -0
- package/dist/types/hooks/translation-context.d.ts +138 -0
- package/dist/types/hooks/translation-context.d.ts.map +1 -0
- package/dist/types/hooks/use-weaverse-data-context.d.ts +29 -0
- package/dist/types/hooks/use-weaverse-data-context.d.ts.map +1 -0
- package/dist/types/index.d.ts +99 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/seo.d.ts +43 -0
- package/dist/types/seo.d.ts.map +1 -0
- package/dist/types/types.d.ts +826 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/utils/index.d.ts +44 -0
- package/dist/types/utils/index.d.ts.map +1 -0
- package/dist/types/utils/normalize-page-url.d.ts +16 -0
- package/dist/types/utils/normalize-page-url.d.ts.map +1 -0
- package/dist/types/utils/pick-weaverse-data.d.ts +44 -0
- package/dist/types/utils/pick-weaverse-data.d.ts.map +1 -0
- package/dist/types/utils/pixel.d.ts +16 -0
- package/dist/types/utils/pixel.d.ts.map +1 -0
- package/dist/types/utils/studio-request-info.d.ts +17 -0
- package/dist/types/utils/studio-request-info.d.ts.map +1 -0
- package/dist/types/utils/studio-script-src.d.ts +78 -0
- package/dist/types/utils/studio-script-src.d.ts.map +1 -0
- package/dist/types/utils/sync-reused-instance.d.ts +21 -0
- package/dist/types/utils/sync-reused-instance.d.ts.map +1 -0
- package/dist/types/utils/theme-text-store.d.ts +2 -0
- package/dist/types/utils/theme-text-store.d.ts.map +1 -0
- package/dist/types/utils/translation-store.d.ts +40 -0
- package/dist/types/utils/translation-store.d.ts.map +1 -0
- package/dist/types/utils/use-studio.d.ts +33 -0
- package/dist/types/utils/use-studio.d.ts.map +1 -0
- package/dist/types/utils/use-theme-settings-store.d.ts +39 -0
- package/dist/types/utils/use-theme-settings-store.d.ts.map +1 -0
- package/dist/types/weaverse-client.d.ts +227 -0
- package/dist/types/weaverse-client.d.ts.map +1 -0
- package/package.json +7 -6
- package/src/WeaverseHydrogenRoot.tsx +649 -0
- package/src/components/index.ts +10 -0
- package/src/components/main.tsx +28 -0
- package/src/hooks/theme-text-context.tsx +14 -0
- package/src/hooks/translation-context.tsx +275 -0
- package/src/hooks/use-weaverse-data-context.ts +69 -0
- package/src/index.ts +179 -0
- package/src/seo.ts +125 -0
- package/src/types.ts +1020 -0
- package/src/utils/index.ts +351 -0
- package/src/utils/normalize-page-url.ts +44 -0
- package/src/utils/pick-weaverse-data.ts +89 -0
- package/src/utils/pixel.ts +71 -0
- package/src/utils/studio-request-info.ts +66 -0
- package/src/utils/studio-script-src.ts +162 -0
- package/src/utils/sync-reused-instance.ts +57 -0
- package/src/utils/theme-text-store.ts +1 -0
- package/src/utils/translation-store.ts +65 -0
- package/src/utils/use-studio.ts +168 -0
- package/src/utils/use-theme-settings-store.ts +160 -0
- package/src/weaverse-client.ts +1179 -0
- package/dist/index.d.mts +0 -1260
- package/dist/index.d.ts +0 -1260
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
import type { ElementData } from '@weaverse/react'
|
|
2
|
+
import {
|
|
3
|
+
isBrowser,
|
|
4
|
+
useSafeExternalStore,
|
|
5
|
+
Weaverse,
|
|
6
|
+
WeaverseItemStore,
|
|
7
|
+
WeaverseRoot,
|
|
8
|
+
} from '@weaverse/react'
|
|
9
|
+
import {
|
|
10
|
+
type ComponentType,
|
|
11
|
+
type JSX,
|
|
12
|
+
memo,
|
|
13
|
+
Suspense,
|
|
14
|
+
useMemo,
|
|
15
|
+
useRef,
|
|
16
|
+
} from 'react'
|
|
17
|
+
import { ErrorBoundary } from 'react-error-boundary'
|
|
18
|
+
import {
|
|
19
|
+
Await,
|
|
20
|
+
useLoaderData,
|
|
21
|
+
useMatches,
|
|
22
|
+
useRouteLoaderData,
|
|
23
|
+
} from 'react-router'
|
|
24
|
+
import { defaultComponents } from './components'
|
|
25
|
+
import {
|
|
26
|
+
type TranslateFunction,
|
|
27
|
+
TranslationProvider,
|
|
28
|
+
} from './hooks/translation-context'
|
|
29
|
+
import { createWeaverseDataContext } from './hooks/use-weaverse-data-context'
|
|
30
|
+
import type {
|
|
31
|
+
HydrogenComponentData,
|
|
32
|
+
HydrogenElement,
|
|
33
|
+
HydrogenPageData,
|
|
34
|
+
HydrogenThemeSettings,
|
|
35
|
+
TranslationChanges,
|
|
36
|
+
TranslationEntry,
|
|
37
|
+
TranslationItemEntry,
|
|
38
|
+
TranslationMap,
|
|
39
|
+
WeaverseInternal,
|
|
40
|
+
WeaverseLoaderRequestInfo,
|
|
41
|
+
} from './index'
|
|
42
|
+
import type {
|
|
43
|
+
HydrogenComponent,
|
|
44
|
+
WeaverseHydrogenParams,
|
|
45
|
+
WeaverseLoaderData,
|
|
46
|
+
} from './types'
|
|
47
|
+
import { hasWeaverseStudio } from './types'
|
|
48
|
+
import { generateDataFromSchema } from './utils'
|
|
49
|
+
import {
|
|
50
|
+
pickWeaverseData,
|
|
51
|
+
type WeaverseDataValue,
|
|
52
|
+
} from './utils/pick-weaverse-data'
|
|
53
|
+
import { normalizeRequestInfo } from './utils/studio-request-info'
|
|
54
|
+
import { syncReusedInstance } from './utils/sync-reused-instance'
|
|
55
|
+
import { TranslationStore } from './utils/translation-store'
|
|
56
|
+
import { useStudio, useStudioConnect } from './utils/use-studio'
|
|
57
|
+
import {
|
|
58
|
+
ThemeSettingsStoreContext,
|
|
59
|
+
useCreateThemeSettingsStore,
|
|
60
|
+
useThemeSettingsStore,
|
|
61
|
+
} from './utils/use-theme-settings-store'
|
|
62
|
+
|
|
63
|
+
/*
|
|
64
|
+
=== IMPORTANT DESIGN PRINCIPLE ===
|
|
65
|
+
CONSTRUCTOR TIMING AND DEFERRED PROCESSING
|
|
66
|
+
|
|
67
|
+
When working with React component systems and registries:
|
|
68
|
+
- Keep constructors SIMPLE - don't do heavy work in them
|
|
69
|
+
- Defer processing until RENDER TIME, not initialization
|
|
70
|
+
- Avoid registry lookups during object construction (components may not be registered yet)
|
|
71
|
+
- Simple code is more reliable than complex upfront optimization
|
|
72
|
+
- Respect the initialization sequence: registration → instance creation → rendering
|
|
73
|
+
|
|
74
|
+
History: Attempting to do schema processing in the constructor (commit 495e1220) broke
|
|
75
|
+
the pilot template because it tried to access the component registry before components were registered.
|
|
76
|
+
The fix was to defer that work until the component is actually rendered.
|
|
77
|
+
====================================
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
/** Runtime store for one rendered Hydrogen component instance. */
|
|
81
|
+
export class WeaverseHydrogenItem extends WeaverseItemStore {
|
|
82
|
+
/** Owning Hydrogen runtime instance. */
|
|
83
|
+
declare weaverse: WeaverseHydrogen
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Cached merged snapshot (base + translations).
|
|
87
|
+
* Reset to `null` whenever `_store` or the item's translation entry changes.
|
|
88
|
+
*/
|
|
89
|
+
private _cachedSnapshot: ElementData | null = null
|
|
90
|
+
private _snapshotStoreRef: ElementData | null = null
|
|
91
|
+
private _snapshotTranslationRef: TranslationItemEntry | null = null
|
|
92
|
+
|
|
93
|
+
/** Creates an item store from serialized component data and its owner. */
|
|
94
|
+
constructor(initialData: HydrogenComponentData, weaverse: WeaverseHydrogen) {
|
|
95
|
+
super(initialData, weaverse)
|
|
96
|
+
const { data, ...rest } = initialData
|
|
97
|
+
if (!this.Element?.schema) {
|
|
98
|
+
console.error('Element is missing schema or not found!')
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
const schemaData = generateDataFromSchema(this.Element.schema)
|
|
102
|
+
Object.assign(this._store, schemaData, data, rest)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Registered component definition for this item's type. */
|
|
106
|
+
get Element(): HydrogenElement {
|
|
107
|
+
return super.Element
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Override getSnapShot to merge translation data when in translation mode.
|
|
112
|
+
* Memoizes the result: returns the same object reference as long as
|
|
113
|
+
* both `_store` and the translation entry haven't changed.
|
|
114
|
+
* This is critical for `useSyncExternalStore` stability.
|
|
115
|
+
*/
|
|
116
|
+
getSnapShot = (): ElementData => {
|
|
117
|
+
const base = this._store
|
|
118
|
+
const translations = this.weaverse.translationMap?.[this._id]
|
|
119
|
+
|
|
120
|
+
// No translations → return base directly (fast path)
|
|
121
|
+
if (!translations) {
|
|
122
|
+
return base
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check if cache is still valid (same _store ref + same translation entry ref)
|
|
126
|
+
if (
|
|
127
|
+
this._cachedSnapshot &&
|
|
128
|
+
this._snapshotStoreRef === base &&
|
|
129
|
+
this._snapshotTranslationRef === translations
|
|
130
|
+
) {
|
|
131
|
+
return this._cachedSnapshot
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Build merged snapshot
|
|
135
|
+
const merged = { ...base }
|
|
136
|
+
for (const [key, entry] of Object.entries(translations)) {
|
|
137
|
+
if (entry.translatedValue !== undefined) {
|
|
138
|
+
merged[key] = entry.translatedValue
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this._cachedSnapshot = merged
|
|
143
|
+
this._snapshotStoreRef = base
|
|
144
|
+
this._snapshotTranslationRef = translations
|
|
145
|
+
return merged
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
Weaverse.ItemConstructor = WeaverseHydrogenItem
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Hydrogen-specific Weaverse runtime that owns page data, component stores,
|
|
153
|
+
* Studio state, and translation sidecars for one rendered page.
|
|
154
|
+
*/
|
|
155
|
+
export class WeaverseHydrogen extends Weaverse {
|
|
156
|
+
/** Identifier of the rendered page. */
|
|
157
|
+
pageId: string
|
|
158
|
+
/** Runtime services and project metadata exposed to integrations. */
|
|
159
|
+
internal: Partial<WeaverseInternal>
|
|
160
|
+
/** Normalized URL and locale information for the current request. */
|
|
161
|
+
requestInfo: WeaverseLoaderRequestInfo
|
|
162
|
+
/** Identifier of the project supplying the page. */
|
|
163
|
+
projectId: string
|
|
164
|
+
/** Origin used for Studio assets and editor communication. */
|
|
165
|
+
weaverseHost: string
|
|
166
|
+
/** Base URL used for public content API requests. */
|
|
167
|
+
weaverseApiBase: string
|
|
168
|
+
/**
|
|
169
|
+
* API key read from environment/configuration for storefront requests or
|
|
170
|
+
* supplied by Studio requests while editing and previewing.
|
|
171
|
+
*/
|
|
172
|
+
weaverseApiKey: string
|
|
173
|
+
/** Studio asset version supplied by the preview request. */
|
|
174
|
+
weaverseVersion: string
|
|
175
|
+
/** Whether the page is running inside the visual editor. */
|
|
176
|
+
isDesignMode: boolean
|
|
177
|
+
/** Whether a single component preview is being rendered. */
|
|
178
|
+
isPreviewMode: boolean
|
|
179
|
+
/** Whether a saved page revision is being previewed. */
|
|
180
|
+
isRevisionPreview: boolean
|
|
181
|
+
/** Component type requested by section preview mode. */
|
|
182
|
+
sectionType: string
|
|
183
|
+
/** Component item store constructor used by the core runtime. */
|
|
184
|
+
declare ItemConstructor: typeof WeaverseHydrogenItem
|
|
185
|
+
/** Current page content owned by the runtime. */
|
|
186
|
+
declare data: HydrogenPageData
|
|
187
|
+
/** Component item stores indexed by instance ID. */
|
|
188
|
+
declare static itemInstances: Map<string, WeaverseHydrogenItem>
|
|
189
|
+
/** Registered component definitions indexed by component type. */
|
|
190
|
+
declare static elementRegistry: Map<string, HydrogenElement>
|
|
191
|
+
|
|
192
|
+
// ─── Translation sidecar (design mode only) ────────────────────────
|
|
193
|
+
/**
|
|
194
|
+
* Translation map received from the builder in design mode.
|
|
195
|
+
* Keyed by item ID, each entry maps field names to translated values.
|
|
196
|
+
* `WeaverseHydrogenItem.getSnapShot()` merges these into the data
|
|
197
|
+
* returned to components, so translations render without monkey-patching.
|
|
198
|
+
*/
|
|
199
|
+
translationMap: TranslationMap = {}
|
|
200
|
+
/** Locale code for the active translation sidecar. */
|
|
201
|
+
translationLocale = ''
|
|
202
|
+
/** Builder language identifier for the active translation sidecar. */
|
|
203
|
+
translationLanguageId = ''
|
|
204
|
+
|
|
205
|
+
/** Creates a page runtime from resolved loader data and request settings. */
|
|
206
|
+
constructor(params: WeaverseHydrogenParams) {
|
|
207
|
+
const { internal, pageId, requestInfo, ...coreParams } = params
|
|
208
|
+
super({ ...coreParams })
|
|
209
|
+
this.internal = internal
|
|
210
|
+
this.pageId = pageId
|
|
211
|
+
this.requestInfo = requestInfo
|
|
212
|
+
// Assign configuration properties
|
|
213
|
+
this.projectId = params.projectId
|
|
214
|
+
this.weaverseHost = params.weaverseHost
|
|
215
|
+
this.weaverseApiBase = params.weaverseApiBase
|
|
216
|
+
this.weaverseApiKey = params.weaverseApiKey
|
|
217
|
+
this.weaverseVersion = params.weaverseVersion || ''
|
|
218
|
+
this.isDesignMode = params.isDesignMode ?? false
|
|
219
|
+
this.isPreviewMode = params.isPreviewMode ?? false
|
|
220
|
+
this.isRevisionPreview = params.isRevisionPreview ?? false
|
|
221
|
+
this.sectionType = params.sectionType || ''
|
|
222
|
+
|
|
223
|
+
// Extract translation sidecar from page data if present (design mode)
|
|
224
|
+
this.extractTranslationSidecar()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Extract translation sidecar data from `this.data` (the page data).
|
|
229
|
+
* The builder attaches `translationMap`, `translationLocale`, and
|
|
230
|
+
* `translationLanguageId` to the page data in design mode.
|
|
231
|
+
*/
|
|
232
|
+
extractTranslationSidecar = () => {
|
|
233
|
+
const pageData = this.data as Record<string, any>
|
|
234
|
+
if (pageData?.translationMap) {
|
|
235
|
+
this.translationMap = pageData.translationMap
|
|
236
|
+
this.translationLocale = pageData.translationLocale || ''
|
|
237
|
+
this.translationLanguageId = pageData.translationLanguageId || ''
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Set the translation sidecar data.
|
|
243
|
+
* Called by the studio bridge when translation data arrives or changes.
|
|
244
|
+
* Triggers a re-render of all items so translated snapshots are recalculated.
|
|
245
|
+
*/
|
|
246
|
+
setTranslationSidecar = (
|
|
247
|
+
translationMap: TranslationMap,
|
|
248
|
+
locale: string,
|
|
249
|
+
languageId: string
|
|
250
|
+
) => {
|
|
251
|
+
this.translationMap = translationMap
|
|
252
|
+
this.translationLocale = locale
|
|
253
|
+
this.translationLanguageId = languageId
|
|
254
|
+
// Invalidate all item snapshot caches and trigger re-render
|
|
255
|
+
this.refreshAllItems()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Update a single translation entry for a specific item field.
|
|
260
|
+
* Used by the studio bridge when the user edits a translatable field.
|
|
261
|
+
*/
|
|
262
|
+
updateTranslation = (
|
|
263
|
+
itemId: string,
|
|
264
|
+
key: string,
|
|
265
|
+
originalValue: string,
|
|
266
|
+
translatedValue: string
|
|
267
|
+
) => {
|
|
268
|
+
if (!this.translationMap[itemId]) {
|
|
269
|
+
this.translationMap[itemId] = {}
|
|
270
|
+
}
|
|
271
|
+
// Create a new entry reference so getSnapShot detects the change
|
|
272
|
+
this.translationMap[itemId] = {
|
|
273
|
+
...this.translationMap[itemId],
|
|
274
|
+
[key]: { originalValue, translatedValue },
|
|
275
|
+
}
|
|
276
|
+
// Trigger re-render for this specific item
|
|
277
|
+
const instance = this.itemInstances.get(itemId)
|
|
278
|
+
if (instance) {
|
|
279
|
+
instance.triggerUpdate()
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Collect all translation changes across all weaverse instances.
|
|
285
|
+
* Used by the save flow to persist translation edits.
|
|
286
|
+
*/
|
|
287
|
+
getTranslationChanges = (): TranslationChanges | undefined => {
|
|
288
|
+
const entries: TranslationEntry[] = []
|
|
289
|
+
const languageId = this.translationLanguageId
|
|
290
|
+
if (!(this.translationLocale && languageId)) {
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
for (const [itemId, fields] of Object.entries(this.translationMap)) {
|
|
295
|
+
for (const [key, entry] of Object.entries(fields)) {
|
|
296
|
+
entries.push({
|
|
297
|
+
itemId,
|
|
298
|
+
key: `data.${key}`,
|
|
299
|
+
originalValue: entry.originalValue,
|
|
300
|
+
translatedValue: entry.translatedValue,
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (entries.length === 0) {
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
return { languageId, entries }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Override setProjectData to re-extract translation sidecar from new page data.
|
|
313
|
+
*/
|
|
314
|
+
setProjectData = (data: HydrogenPageData) => {
|
|
315
|
+
this.data = data
|
|
316
|
+
this.extractTranslationSidecar()
|
|
317
|
+
this.initProject()
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function createWeaverseInstance(
|
|
322
|
+
params: WeaverseHydrogenParams
|
|
323
|
+
): WeaverseHydrogen {
|
|
324
|
+
const normalizedParams = normalizeRequestInfo(params)
|
|
325
|
+
if (isBrowser) {
|
|
326
|
+
window.__weaverses = window.__weaverses || {}
|
|
327
|
+
const existing = window.__weaverses[normalizedParams.pageId]
|
|
328
|
+
// Reuse the instance only while the browser stays on the same URL —
|
|
329
|
+
// navigation to a different pathname/search builds a fresh instance.
|
|
330
|
+
const isReused =
|
|
331
|
+
existing?.requestInfo?.pathname ===
|
|
332
|
+
normalizedParams.requestInfo.pathname &&
|
|
333
|
+
existing?.requestInfo?.search === normalizedParams.requestInfo.search
|
|
334
|
+
const weaverse = isReused
|
|
335
|
+
? existing
|
|
336
|
+
: new WeaverseHydrogen(normalizedParams)
|
|
337
|
+
window.__weaverses[normalizedParams.pageId] = weaverse
|
|
338
|
+
if (weaverse.isDesignMode) {
|
|
339
|
+
weaverse.requestInfo = normalizedParams.requestInfo
|
|
340
|
+
if (hasWeaverseStudio(window)) {
|
|
341
|
+
window.weaverseStudio.refreshStudio(normalizedParams)
|
|
342
|
+
}
|
|
343
|
+
} else if (isReused) {
|
|
344
|
+
// Same-URL revalidation in live/preview mode: the reused instance
|
|
345
|
+
// must adopt the fresh loader payload (see syncReusedInstance).
|
|
346
|
+
syncReusedInstance(weaverse, normalizedParams)
|
|
347
|
+
}
|
|
348
|
+
return weaverse
|
|
349
|
+
}
|
|
350
|
+
return new WeaverseHydrogen(normalizedParams)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const StudioBridge = memo(({ context }: { context: WeaverseHydrogen }) => {
|
|
354
|
+
useStudio(context)
|
|
355
|
+
return null
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
const StudioConnect = memo(() => {
|
|
359
|
+
useStudioConnect()
|
|
360
|
+
return null
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Component lists already pushed into the element registry. Registration is
|
|
365
|
+
* idempotent (the registry checks `has(type)`), but walking the full list
|
|
366
|
+
* allocates and loops on every render — theme component arrays are
|
|
367
|
+
* module-level constants, so register each list identity once.
|
|
368
|
+
*/
|
|
369
|
+
const registeredComponentLists = new WeakSet<HydrogenComponent[]>()
|
|
370
|
+
|
|
371
|
+
function RenderRoot(props: {
|
|
372
|
+
data: WeaverseLoaderData
|
|
373
|
+
components: HydrogenComponent[]
|
|
374
|
+
dataContext?: Record<string, any>
|
|
375
|
+
}) {
|
|
376
|
+
const { data, components, dataContext } = props
|
|
377
|
+
if (!registeredComponentLists.has(components)) {
|
|
378
|
+
for (const comp of [...components, ...defaultComponents]) {
|
|
379
|
+
if (comp?.schema?.type) {
|
|
380
|
+
registerComponent({
|
|
381
|
+
type: comp.schema.type,
|
|
382
|
+
Component: comp.default,
|
|
383
|
+
schema: comp.schema,
|
|
384
|
+
loader: comp.loader,
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
registeredComponentLists.add(components)
|
|
389
|
+
}
|
|
390
|
+
const { page, configs, project, pageAssignment } = data || {}
|
|
391
|
+
|
|
392
|
+
// Keep original page data - processing will happen at component level in renderer
|
|
393
|
+
|
|
394
|
+
const weaverse = createWeaverseInstance({
|
|
395
|
+
...configs,
|
|
396
|
+
dataContext,
|
|
397
|
+
data: page,
|
|
398
|
+
pageId: page?.id,
|
|
399
|
+
internal: { project, pageAssignment },
|
|
400
|
+
})
|
|
401
|
+
return (
|
|
402
|
+
<>
|
|
403
|
+
<WeaverseRoot context={weaverse} />
|
|
404
|
+
<StudioBridge context={weaverse} />
|
|
405
|
+
</>
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Register a Weaverse component with the global element registry.
|
|
411
|
+
* Must be called before rendering components in WeaverseHydrogenRoot.
|
|
412
|
+
*
|
|
413
|
+
* @param element - Component configuration with schema, Component, and optional loader
|
|
414
|
+
*
|
|
415
|
+
* @example
|
|
416
|
+
* ```typescript
|
|
417
|
+
* import Hero from './sections/Hero'
|
|
418
|
+
* import { schema } from './sections/Hero'
|
|
419
|
+
*
|
|
420
|
+
* registerComponent({
|
|
421
|
+
* type: 'hero',
|
|
422
|
+
* Component: Hero,
|
|
423
|
+
* schema,
|
|
424
|
+
* loader: async ({ data, weaverse }) => {
|
|
425
|
+
* return { someData: 'value' }
|
|
426
|
+
* }
|
|
427
|
+
* })
|
|
428
|
+
* ```
|
|
429
|
+
*/
|
|
430
|
+
export function registerComponent(element: HydrogenElement) {
|
|
431
|
+
WeaverseHydrogen.registerElement(element)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Props passed to a custom Weaverse page error component. */
|
|
435
|
+
export interface WeaverseHydrogenRootErrorProps {
|
|
436
|
+
/** Error raised while resolving or rendering the Weaverse page. */
|
|
437
|
+
error: Error | unknown
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Props accepted by {@link WeaverseHydrogenRoot}. */
|
|
441
|
+
export interface WeaverseHydrogenRootProps {
|
|
442
|
+
/** Theme components available to the page renderer. */
|
|
443
|
+
components: HydrogenComponent[]
|
|
444
|
+
/**
|
|
445
|
+
* Optional explicit `weaverseData` payload. When provided, bypasses the
|
|
446
|
+
* automatic route-scoped resolution. Accepts a resolved value, a
|
|
447
|
+
* `Promise` (handled via `<Await>`), or `null` to suppress rendering.
|
|
448
|
+
*
|
|
449
|
+
* Recommended for nested layout + child route compositions where two
|
|
450
|
+
* `<WeaverseHydrogenRoot>` instances need to render different Weaverse
|
|
451
|
+
* pages on the same URL. See issue #451.
|
|
452
|
+
*/
|
|
453
|
+
data?: WeaverseDataValue
|
|
454
|
+
/** Component rendered when page data cannot be resolved or awaited. */
|
|
455
|
+
errorComponent?: React.FC<WeaverseHydrogenRootErrorProps>
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Renders the Weaverse page for the current React Router route.
|
|
460
|
+
*
|
|
461
|
+
* Pass `data` to override automatic route-loader resolution. A promise is
|
|
462
|
+
* handled with React Router's `Await`, while explicit `null` suppresses output.
|
|
463
|
+
*/
|
|
464
|
+
export const WeaverseHydrogenRoot = memo(
|
|
465
|
+
({
|
|
466
|
+
components,
|
|
467
|
+
data: dataProp,
|
|
468
|
+
errorComponent: ErrorComponent = ({ error }) => (
|
|
469
|
+
<div>
|
|
470
|
+
{error instanceof Error
|
|
471
|
+
? error.message
|
|
472
|
+
: 'An unexpected error occurred'}
|
|
473
|
+
</div>
|
|
474
|
+
),
|
|
475
|
+
}: WeaverseHydrogenRootProps) => {
|
|
476
|
+
const matches = useMatches()
|
|
477
|
+
|
|
478
|
+
// Flat route-keyed data context for data-connector template resolution
|
|
479
|
+
// (e.g. {{root.layout.shop.name}}). Legitimately needs all matches;
|
|
480
|
+
// memoized so same-matches re-renders don't allocate a fresh context.
|
|
481
|
+
const enhancedDataContext = useMemo(
|
|
482
|
+
() => createWeaverseDataContext(matches),
|
|
483
|
+
[matches]
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
// Route-scoped weaverseData lookup. `useLoaderData()` returns the
|
|
487
|
+
// closest route's loader data, which is what makes nested layout +
|
|
488
|
+
// child routes work — each <WeaverseHydrogenRoot> instance resolves
|
|
489
|
+
// to its own route's weaverseData. See pickWeaverseData() for the
|
|
490
|
+
// three-tier fallback policy.
|
|
491
|
+
const ownLoaderData = useLoaderData() as unknown
|
|
492
|
+
const weaverseData = useMemo(
|
|
493
|
+
() => pickWeaverseData(dataProp, ownLoaderData, matches),
|
|
494
|
+
[dataProp, ownLoaderData, matches]
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
// Explicit `data={null}` is the documented signal for "suppress rendering"
|
|
498
|
+
// (e.g. a route conditionally disables Weaverse without unmounting the
|
|
499
|
+
// component). This is distinct from auto-resolution returning `null`,
|
|
500
|
+
// which means "data was expected but not found" — the error path below.
|
|
501
|
+
if (dataProp === null) {
|
|
502
|
+
return null
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (weaverseData) {
|
|
506
|
+
if (weaverseData instanceof Promise) {
|
|
507
|
+
return (
|
|
508
|
+
<ErrorBoundary fallbackRender={ErrorComponent as any}>
|
|
509
|
+
<Suspense>
|
|
510
|
+
<Await resolve={weaverseData}>
|
|
511
|
+
{(resolvedData) => (
|
|
512
|
+
<RenderRoot
|
|
513
|
+
components={components}
|
|
514
|
+
data={resolvedData}
|
|
515
|
+
dataContext={enhancedDataContext}
|
|
516
|
+
/>
|
|
517
|
+
)}
|
|
518
|
+
</Await>
|
|
519
|
+
</Suspense>
|
|
520
|
+
</ErrorBoundary>
|
|
521
|
+
)
|
|
522
|
+
}
|
|
523
|
+
return (
|
|
524
|
+
<RenderRoot
|
|
525
|
+
components={components}
|
|
526
|
+
data={weaverseData}
|
|
527
|
+
dataContext={enhancedDataContext}
|
|
528
|
+
/>
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
return (
|
|
532
|
+
<ErrorComponent
|
|
533
|
+
error={{ message: 'No Weaverse data found in route matches!' }}
|
|
534
|
+
/>
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Options for the `withWeaverse` HOC.
|
|
541
|
+
*/
|
|
542
|
+
export type WithWeaverseOptions = {
|
|
543
|
+
/**
|
|
544
|
+
* Optional external translation function (e.g. from Shopify i18n).
|
|
545
|
+
* When provided it becomes the **highest-priority** source
|
|
546
|
+
* in the `t()` resolution chain exposed by `useTranslation()`.
|
|
547
|
+
*/
|
|
548
|
+
t?: TranslateFunction
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Higher-order component that wraps your app with Weaverse theme settings and
|
|
553
|
+
* translation context, and mounts the Studio connect bridge.
|
|
554
|
+
*
|
|
555
|
+
* Export the result as your root route's **`Layout`** (not the default route
|
|
556
|
+
* `Component`). In React Router the root `Layout` wraps both the matched route
|
|
557
|
+
* and its `ErrorBoundary`, so the Studio bridge keeps rendering on 404s and
|
|
558
|
+
* loader/render errors — exactly the pages where Studio must still answer its
|
|
559
|
+
* handshake instead of reporting a false "Connection lost". A default-component
|
|
560
|
+
* wrapper (`export default withWeaverse(App)`) is replaced by the error
|
|
561
|
+
* boundary on those routes and would drop the bridge.
|
|
562
|
+
*
|
|
563
|
+
* @param Component - The root layout component to wrap
|
|
564
|
+
* @param options - Optional configuration (e.g. an external `t` function)
|
|
565
|
+
* @returns Wrapped component with theme settings + translation providers
|
|
566
|
+
*
|
|
567
|
+
* @example
|
|
568
|
+
* ```typescript
|
|
569
|
+
* import { withWeaverse } from '@weaverse/hydrogen'
|
|
570
|
+
*
|
|
571
|
+
* // Basic usage — export as the root route Layout
|
|
572
|
+
* export const Layout = withWeaverse(RootLayout)
|
|
573
|
+
*
|
|
574
|
+
* // With an external translation function
|
|
575
|
+
* export const Layout = withWeaverse(RootLayout, { t: myI18nFunction })
|
|
576
|
+
* ```
|
|
577
|
+
*/
|
|
578
|
+
export function withWeaverse(
|
|
579
|
+
Component: ComponentType<any>,
|
|
580
|
+
options?: WithWeaverseOptions
|
|
581
|
+
) {
|
|
582
|
+
return (props: JSX.IntrinsicAttributes) => {
|
|
583
|
+
const themeSettingsStore = useCreateThemeSettingsStore()
|
|
584
|
+
const translationData = useTranslationData()
|
|
585
|
+
const translationStoreRef = useRef<TranslationStore | null>(null)
|
|
586
|
+
if (!translationStoreRef.current) {
|
|
587
|
+
translationStoreRef.current = new TranslationStore()
|
|
588
|
+
}
|
|
589
|
+
return (
|
|
590
|
+
<ThemeSettingsStoreContext.Provider value={themeSettingsStore}>
|
|
591
|
+
<TranslationProvider
|
|
592
|
+
merchantOverrides={translationData.merchantOverrides}
|
|
593
|
+
staticContent={translationData.staticContent}
|
|
594
|
+
t={options?.t}
|
|
595
|
+
translationStore={translationStoreRef.current}
|
|
596
|
+
>
|
|
597
|
+
<StudioConnect />
|
|
598
|
+
<Component {...props} />
|
|
599
|
+
</TranslationProvider>
|
|
600
|
+
</ThemeSettingsStoreContext.Provider>
|
|
601
|
+
)
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Internal hook to read staticContent and merchantOverrides from root loader data.
|
|
607
|
+
* Similar pattern to useThemeSettingsStore – reads from the 'root' route loader.
|
|
608
|
+
*/
|
|
609
|
+
function useTranslationData(): {
|
|
610
|
+
staticContent: Record<string, unknown>
|
|
611
|
+
merchantOverrides?: Record<string, unknown>
|
|
612
|
+
} {
|
|
613
|
+
const data = useRouteLoaderData('root') as {
|
|
614
|
+
weaverseTheme?: {
|
|
615
|
+
staticContent?: Record<string, unknown>
|
|
616
|
+
merchantOverrides?: Record<string, unknown>
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
staticContent: data?.weaverseTheme?.staticContent ?? {},
|
|
621
|
+
merchantOverrides: data?.weaverseTheme?.merchantOverrides,
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* React hook to access Weaverse theme settings from anywhere in your component tree.
|
|
627
|
+
* Must be used within a component wrapped by `withWeaverse` or inside `WeaverseHydrogenRoot`.
|
|
628
|
+
*
|
|
629
|
+
* @returns Current theme settings object
|
|
630
|
+
*
|
|
631
|
+
* @example
|
|
632
|
+
* ```typescript
|
|
633
|
+
* function MyComponent() {
|
|
634
|
+
* const theme = useThemeSettings()
|
|
635
|
+
* const primaryColor = theme.colors?.primary
|
|
636
|
+
*
|
|
637
|
+
* return <div style={{ color: primaryColor }}>Styled content</div>
|
|
638
|
+
* }
|
|
639
|
+
* ```
|
|
640
|
+
*/
|
|
641
|
+
export function useThemeSettings<T = HydrogenThemeSettings>() {
|
|
642
|
+
const themeSettingsStore = useThemeSettingsStore()
|
|
643
|
+
const settings = useSafeExternalStore(
|
|
644
|
+
themeSettingsStore.subscribe,
|
|
645
|
+
themeSettingsStore.getSnapshot,
|
|
646
|
+
themeSettingsStore.getServerSnapshot
|
|
647
|
+
)
|
|
648
|
+
return settings as T
|
|
649
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HydrogenComponent } from '../types'
|
|
2
|
+
import MainComponent, { schema as mainSchema } from './main'
|
|
3
|
+
|
|
4
|
+
/** Built-in components required to render the root of every Weaverse page. */
|
|
5
|
+
export let defaultComponents: HydrogenComponent[] = [
|
|
6
|
+
{
|
|
7
|
+
default: MainComponent,
|
|
8
|
+
schema: mainSchema,
|
|
9
|
+
},
|
|
10
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createSchema, type SchemaType } from '@weaverse/schema'
|
|
2
|
+
import type { HTMLAttributes } from 'react'
|
|
3
|
+
import type { HydrogenComponentProps } from '../types'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Main is the default Weaverse component that is used to render the main content.
|
|
7
|
+
This component wraps all the sections/components inside a Weaverse page.
|
|
8
|
+
*/
|
|
9
|
+
function Main(props: HydrogenComponentProps & HTMLAttributes<HTMLDivElement>) {
|
|
10
|
+
let { children, dangerouslySetInnerHTML, ...rest } = props
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
{...rest}
|
|
15
|
+
// biome-ignore lint/correctness/noChildrenProp: <explanation>
|
|
16
|
+
children={children}
|
|
17
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtmlWithChildren: <explanation>
|
|
18
|
+
dangerouslySetInnerHTML={children ? undefined : dangerouslySetInnerHTML}
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default Main
|
|
24
|
+
|
|
25
|
+
export let schema: SchemaType = createSchema({
|
|
26
|
+
type: 'main',
|
|
27
|
+
title: 'Main',
|
|
28
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createTranslate,
|
|
3
|
+
getNestedKey,
|
|
4
|
+
interpolate,
|
|
5
|
+
ThemeTextProvider,
|
|
6
|
+
type ThemeTextProviderProps,
|
|
7
|
+
type ThemeTextValue,
|
|
8
|
+
type TranslateFunction,
|
|
9
|
+
TranslationProvider,
|
|
10
|
+
type TranslationProviderProps,
|
|
11
|
+
type TranslationValue,
|
|
12
|
+
useThemeText,
|
|
13
|
+
useTranslation,
|
|
14
|
+
} from './translation-context'
|