@weaverse/hydrogen 5.17.1 → 5.19.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.
Files changed (70) hide show
  1. package/dist/index.js +83 -1
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +83 -1
  4. package/dist/index.mjs.map +1 -1
  5. package/dist/types/WeaverseHydrogenRoot.d.ts +218 -0
  6. package/dist/types/WeaverseHydrogenRoot.d.ts.map +1 -0
  7. package/dist/types/components/index.d.ts +4 -0
  8. package/dist/types/components/index.d.ts.map +1 -0
  9. package/dist/types/components/main.d.ts +7 -0
  10. package/dist/types/components/main.d.ts.map +1 -0
  11. package/dist/types/hooks/theme-text-context.d.ts +2 -0
  12. package/dist/types/hooks/theme-text-context.d.ts.map +1 -0
  13. package/dist/types/hooks/translation-context.d.ts +138 -0
  14. package/dist/types/hooks/translation-context.d.ts.map +1 -0
  15. package/dist/types/hooks/use-weaverse-data-context.d.ts +29 -0
  16. package/dist/types/hooks/use-weaverse-data-context.d.ts.map +1 -0
  17. package/dist/types/index.d.ts +99 -0
  18. package/dist/types/index.d.ts.map +1 -0
  19. package/dist/types/seo.d.ts +43 -0
  20. package/dist/types/seo.d.ts.map +1 -0
  21. package/dist/types/types.d.ts +826 -0
  22. package/dist/types/types.d.ts.map +1 -0
  23. package/dist/types/utils/index.d.ts +44 -0
  24. package/dist/types/utils/index.d.ts.map +1 -0
  25. package/dist/types/utils/normalize-page-url.d.ts +16 -0
  26. package/dist/types/utils/normalize-page-url.d.ts.map +1 -0
  27. package/dist/types/utils/pick-weaverse-data.d.ts +44 -0
  28. package/dist/types/utils/pick-weaverse-data.d.ts.map +1 -0
  29. package/dist/types/utils/pixel.d.ts +16 -0
  30. package/dist/types/utils/pixel.d.ts.map +1 -0
  31. package/dist/types/utils/studio-request-info.d.ts +17 -0
  32. package/dist/types/utils/studio-request-info.d.ts.map +1 -0
  33. package/dist/types/utils/studio-script-src.d.ts +78 -0
  34. package/dist/types/utils/studio-script-src.d.ts.map +1 -0
  35. package/dist/types/utils/sync-reused-instance.d.ts +21 -0
  36. package/dist/types/utils/sync-reused-instance.d.ts.map +1 -0
  37. package/dist/types/utils/theme-text-store.d.ts +2 -0
  38. package/dist/types/utils/theme-text-store.d.ts.map +1 -0
  39. package/dist/types/utils/translation-store.d.ts +40 -0
  40. package/dist/types/utils/translation-store.d.ts.map +1 -0
  41. package/dist/types/utils/use-studio.d.ts +33 -0
  42. package/dist/types/utils/use-studio.d.ts.map +1 -0
  43. package/dist/types/utils/use-theme-settings-store.d.ts +39 -0
  44. package/dist/types/utils/use-theme-settings-store.d.ts.map +1 -0
  45. package/dist/types/weaverse-client.d.ts +227 -0
  46. package/dist/types/weaverse-client.d.ts.map +1 -0
  47. package/package.json +7 -6
  48. package/src/WeaverseHydrogenRoot.tsx +649 -0
  49. package/src/components/index.ts +10 -0
  50. package/src/components/main.tsx +28 -0
  51. package/src/hooks/theme-text-context.tsx +14 -0
  52. package/src/hooks/translation-context.tsx +275 -0
  53. package/src/hooks/use-weaverse-data-context.ts +69 -0
  54. package/src/index.ts +179 -0
  55. package/src/seo.ts +125 -0
  56. package/src/types.ts +1020 -0
  57. package/src/utils/index.ts +351 -0
  58. package/src/utils/normalize-page-url.ts +44 -0
  59. package/src/utils/pick-weaverse-data.ts +89 -0
  60. package/src/utils/pixel.ts +71 -0
  61. package/src/utils/studio-request-info.ts +66 -0
  62. package/src/utils/studio-script-src.ts +162 -0
  63. package/src/utils/sync-reused-instance.ts +57 -0
  64. package/src/utils/theme-text-store.ts +1 -0
  65. package/src/utils/translation-store.ts +65 -0
  66. package/src/utils/use-studio.ts +168 -0
  67. package/src/utils/use-theme-settings-store.ts +160 -0
  68. package/src/weaverse-client.ts +1179 -0
  69. package/dist/index.d.mts +0 -1260
  70. 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'