@weaverse/hydrogen 6.1.3 → 6.2.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 (142) hide show
  1. package/dist/index.js +5 -6
  2. package/dist/index.mjs +1 -2
  3. package/dist/internal.js +18 -29
  4. package/dist/internal.mjs +3 -14
  5. package/dist/next-client-boundary.js +1088 -0
  6. package/dist/next-client-boundary.js.map +1 -0
  7. package/dist/next-client-boundary.mjs +1038 -0
  8. package/dist/next-client-boundary.mjs.map +1 -0
  9. package/dist/next-server.js +669 -13
  10. package/dist/next-server.js.map +1 -0
  11. package/dist/next-server.mjs +666 -2
  12. package/dist/next-server.mjs.map +1 -0
  13. package/dist/next.js +29 -43
  14. package/dist/next.mjs +5 -3
  15. package/dist/react-router.js +6 -7
  16. package/dist/react-router.mjs +2 -3
  17. package/dist/seo-B8u2r3rX.mjs +205 -0
  18. package/dist/seo-B8u2r3rX.mjs.map +1 -0
  19. package/dist/seo-D9A9uRQ4.js +264 -0
  20. package/dist/seo-D9A9uRQ4.js.map +1 -0
  21. package/dist/translate-Celryv8j.mjs +26 -0
  22. package/dist/translate-Celryv8j.mjs.map +1 -0
  23. package/dist/translate-Kt1BYL1n.js +37 -0
  24. package/dist/translate-Kt1BYL1n.js.map +1 -0
  25. package/dist/{nested-key-CR3-YMFi.js → translation-store-BmGxBALf.js} +46 -137
  26. package/dist/translation-store-BmGxBALf.js.map +1 -0
  27. package/dist/{nested-key-_k9aW_Wd.mjs → translation-store-JFs-4lvf.mjs} +35 -138
  28. package/dist/translation-store-JFs-4lvf.mjs.map +1 -0
  29. package/dist/types/hooks/translation-context.d.ts +2 -36
  30. package/dist/types/hooks/translation-context.d.ts.map +1 -1
  31. package/dist/types/next/client-boundary.d.ts +25 -0
  32. package/dist/types/next/client-boundary.d.ts.map +1 -0
  33. package/dist/types/next/client.d.ts +16 -0
  34. package/dist/types/next/client.d.ts.map +1 -0
  35. package/dist/types/next/hooks.d.ts +20 -0
  36. package/dist/types/next/hooks.d.ts.map +1 -0
  37. package/dist/types/next/index.d.ts +34 -0
  38. package/dist/types/next/index.d.ts.map +1 -0
  39. package/dist/types/next/item.d.ts +60 -0
  40. package/dist/types/next/item.d.ts.map +1 -0
  41. package/dist/types/next/loader.d.ts +25 -0
  42. package/dist/types/next/loader.d.ts.map +1 -0
  43. package/dist/types/next/main.d.ts +11 -0
  44. package/dist/types/next/main.d.ts.map +1 -0
  45. package/dist/types/next/provider.d.ts +85 -0
  46. package/dist/types/next/provider.d.ts.map +1 -0
  47. package/dist/types/next/registry.d.ts +15 -0
  48. package/dist/types/next/registry.d.ts.map +1 -0
  49. package/dist/types/next/renderer.d.ts +21 -0
  50. package/dist/types/next/renderer.d.ts.map +1 -0
  51. package/dist/types/next/request-info.d.ts +19 -0
  52. package/dist/types/next/request-info.d.ts.map +1 -0
  53. package/dist/types/next/revalidate-item.d.ts +82 -0
  54. package/dist/types/next/revalidate-item.d.ts.map +1 -0
  55. package/dist/types/next/root-provider.d.ts +65 -0
  56. package/dist/types/next/root-provider.d.ts.map +1 -0
  57. package/dist/types/next/runtime.d.ts +113 -0
  58. package/dist/types/next/runtime.d.ts.map +1 -0
  59. package/dist/types/next/seo.d.ts +55 -0
  60. package/dist/types/next/seo.d.ts.map +1 -0
  61. package/dist/types/next/server/configs.d.ts +27 -0
  62. package/dist/types/next/server/configs.d.ts.map +1 -0
  63. package/dist/types/next/server/normalize-page-url.d.ts +19 -0
  64. package/dist/types/next/server/normalize-page-url.d.ts.map +1 -0
  65. package/dist/types/next/server/revalidate-handler.d.ts +55 -0
  66. package/dist/types/next/server/revalidate-handler.d.ts.map +1 -0
  67. package/dist/types/next/server/server-client.d.ts +21 -0
  68. package/dist/types/next/server/server-client.d.ts.map +1 -0
  69. package/dist/types/next/server.d.ts +19 -0
  70. package/dist/types/next/server.d.ts.map +1 -0
  71. package/dist/types/next/studio-bridge.d.ts +16 -0
  72. package/dist/types/next/studio-bridge.d.ts.map +1 -0
  73. package/dist/types/next/studio-connect.d.ts +27 -0
  74. package/dist/types/next/studio-connect.d.ts.map +1 -0
  75. package/dist/types/next/studio-router.d.ts +50 -0
  76. package/dist/types/next/studio-router.d.ts.map +1 -0
  77. package/dist/types/next/studio-script-src.d.ts +12 -0
  78. package/dist/types/next/studio-script-src.d.ts.map +1 -0
  79. package/dist/types/next/theme-settings-store.d.ts +18 -0
  80. package/dist/types/next/theme-settings-store.d.ts.map +1 -0
  81. package/dist/types/next/translation-context.d.ts +109 -0
  82. package/dist/types/next/translation-context.d.ts.map +1 -0
  83. package/dist/types/next/types.d.ts +674 -0
  84. package/dist/types/next/types.d.ts.map +1 -0
  85. package/dist/types/next/use-weaverse-next-studio.d.ts +34 -0
  86. package/dist/types/next/use-weaverse-next-studio.d.ts.map +1 -0
  87. package/dist/types/next/utils.d.ts +21 -0
  88. package/dist/types/next/utils.d.ts.map +1 -0
  89. package/dist/types/next-server.d.ts +3 -1
  90. package/dist/types/next-server.d.ts.map +1 -1
  91. package/dist/types/next.d.ts +4 -2
  92. package/dist/types/next.d.ts.map +1 -1
  93. package/dist/types/utils/translate.d.ts +51 -0
  94. package/dist/types/utils/translate.d.ts.map +1 -0
  95. package/dist/{weaverse-client-C9IgIH7O.js → weaverse-client-BmceZc5q.js} +49 -771
  96. package/dist/weaverse-client-BmceZc5q.js.map +1 -0
  97. package/dist/{weaverse-client-D59mBW0j.mjs → weaverse-client-CXnM9ayH.mjs} +4 -720
  98. package/dist/weaverse-client-CXnM9ayH.mjs.map +1 -0
  99. package/package.json +13 -7
  100. package/src/hooks/translation-context.tsx +7 -84
  101. package/src/next/client-boundary.ts +51 -0
  102. package/src/next/client.ts +102 -0
  103. package/src/next/hooks.ts +52 -0
  104. package/src/next/index.ts +145 -0
  105. package/src/next/item.ts +167 -0
  106. package/src/next/loader.ts +135 -0
  107. package/src/next/main.tsx +45 -0
  108. package/src/next/provider.tsx +241 -0
  109. package/src/next/registry.ts +63 -0
  110. package/src/next/renderer.tsx +102 -0
  111. package/src/next/request-info.ts +124 -0
  112. package/src/next/revalidate-item.ts +211 -0
  113. package/src/next/root-provider.tsx +201 -0
  114. package/src/next/runtime.ts +559 -0
  115. package/src/next/seo.ts +157 -0
  116. package/src/next/server/configs.ts +178 -0
  117. package/src/next/server/normalize-page-url.ts +73 -0
  118. package/src/next/server/revalidate-handler.ts +357 -0
  119. package/src/next/server/server-client.ts +728 -0
  120. package/src/next/server.ts +67 -0
  121. package/src/next/studio-bridge.tsx +82 -0
  122. package/src/next/studio-connect.tsx +88 -0
  123. package/src/next/studio-router.ts +85 -0
  124. package/src/next/studio-script-src.ts +128 -0
  125. package/src/next/theme-settings-store.ts +47 -0
  126. package/src/next/translation-context.tsx +182 -0
  127. package/src/next/types.ts +764 -0
  128. package/src/next/use-weaverse-next-studio.tsx +106 -0
  129. package/src/next/utils.ts +48 -0
  130. package/src/next-server.ts +41 -1
  131. package/src/next.ts +48 -12
  132. package/src/utils/translate.ts +101 -0
  133. package/dist/internal.js.map +0 -1
  134. package/dist/internal.mjs.map +0 -1
  135. package/dist/nested-key-CR3-YMFi.js.map +0 -1
  136. package/dist/nested-key-_k9aW_Wd.mjs.map +0 -1
  137. package/dist/translation-store-4fAV771j.js +0 -69
  138. package/dist/translation-store-4fAV771j.js.map +0 -1
  139. package/dist/translation-store-BBrw0FOk.mjs +0 -58
  140. package/dist/translation-store-BBrw0FOk.mjs.map +0 -1
  141. package/dist/weaverse-client-C9IgIH7O.js.map +0 -1
  142. package/dist/weaverse-client-D59mBW0j.mjs.map +0 -1
@@ -0,0 +1,559 @@
1
+ import { Weaverse } from '@weaverse/react'
2
+ import { TranslationStore } from '../utils/translation-store'
3
+ import {
4
+ collectDeferredItemUpdates,
5
+ ensureNextItemConstructor,
6
+ type WeaverseNextItem,
7
+ } from './item'
8
+ import { buildWeaverseNextRequestInfo } from './request-info'
9
+ import { createWeaverseNextThemeSettingsStore } from './theme-settings-store'
10
+ import type {
11
+ WeaverseNextClient,
12
+ WeaverseNextLoaderData,
13
+ WeaverseNextPageData,
14
+ WeaverseNextRequestInfo,
15
+ WeaverseNextRuntimeConfig,
16
+ WeaverseNextRuntimeInternal,
17
+ WeaverseNextTranslationChanges,
18
+ WeaverseNextTranslationEntry,
19
+ WeaverseNextTranslationMap,
20
+ } from './types'
21
+
22
+ declare global {
23
+ interface Window {
24
+ __weaverse?: WeaverseNextRuntime
25
+ __weaverses?: Record<string, WeaverseNextRuntime>
26
+ }
27
+ }
28
+
29
+ type StudioBoundRuntime = WeaverseNextRuntime & {
30
+ __weaverseNextStudioBound?: boolean
31
+ __weaverseNextRequestKey?: string
32
+ __weaverseNextLatestData?: WeaverseNextRenderablePage
33
+ }
34
+
35
+ function getRuntimeWindow(): Window | undefined {
36
+ return typeof window === 'undefined' ? undefined : window
37
+ }
38
+
39
+ function getRenderablePage(
40
+ data: WeaverseNextLoaderData
41
+ ): WeaverseNextRenderablePage {
42
+ let page = data.page
43
+ let rootId =
44
+ page.rootId ??
45
+ page.items.find((item) => item.type === 'main')?.id ??
46
+ page.items[0]?.id ??
47
+ page.id
48
+ return { ...page, rootId }
49
+ }
50
+
51
+ function getConfigString(
52
+ configs: Record<string, unknown> | undefined,
53
+ key: string
54
+ ): string | undefined {
55
+ let value = configs?.[key]
56
+ return typeof value === 'string' ? value : undefined
57
+ }
58
+
59
+ /**
60
+ * Resolve a non-published mode flag from the request context OR the serialized
61
+ * loader configs.
62
+ *
63
+ * These flags only ever suppress Studio rendering, so the two sources are
64
+ * OR-ed rather than nullish-coalesced. A
65
+ * client-supplied `requestContext` that explicitly sets `false` must not shadow
66
+ * a `true` the server resolved for this request.
67
+ */
68
+ function resolveModeFlag(
69
+ config: WeaverseNextRuntimeConfig,
70
+ key: 'isDesignMode' | 'isPreviewMode' | 'isRevisionPreview'
71
+ ): boolean {
72
+ return (
73
+ config.client?.requestContext?.[key] === true ||
74
+ config.data.configs?.[key] === true
75
+ )
76
+ }
77
+
78
+ /** Section preview type from either source; see {@link resolveModeFlag}. */
79
+ function resolveSectionType(
80
+ config: WeaverseNextRuntimeConfig
81
+ ): string | undefined {
82
+ return (
83
+ config.client?.requestContext?.sectionType ||
84
+ getConfigString(config.data.configs, 'sectionType') ||
85
+ undefined
86
+ )
87
+ }
88
+
89
+ function getRecordString(
90
+ record: Record<string, unknown> | undefined,
91
+ key: string
92
+ ): string | undefined {
93
+ let value = record?.[key]
94
+ return typeof value === 'string' ? value : undefined
95
+ }
96
+
97
+ function resolveProjectId(
98
+ client: WeaverseNextClient | undefined,
99
+ data: WeaverseNextLoaderData,
100
+ fallback: string
101
+ ): string {
102
+ let projectId = client?.projectId
103
+ if (projectId) {
104
+ return projectId
105
+ }
106
+
107
+ let configProjectId = getConfigString(data.configs, 'projectId')
108
+ if (configProjectId) {
109
+ return configProjectId
110
+ }
111
+
112
+ return getRecordString(data.project, 'id') ?? fallback
113
+ }
114
+
115
+ function resolveDataContext(config: WeaverseNextRuntimeConfig) {
116
+ if (config.dataContext) {
117
+ return config.dataContext
118
+ }
119
+ if (config.client?.dataContext) {
120
+ return config.client.dataContext
121
+ }
122
+ return {}
123
+ }
124
+
125
+ function getRuntimeKey(pageId: string, requestInfo: WeaverseNextRequestInfo) {
126
+ return `${pageId}:${requestInfo.pathname}:${requestInfo.search}`
127
+ }
128
+
129
+ function registerRuntime(runtime: WeaverseNextRuntime) {
130
+ let runtimeWindow = getRuntimeWindow()
131
+ if (!runtimeWindow) {
132
+ return
133
+ }
134
+
135
+ runtimeWindow.__weaverses ??= {}
136
+ runtimeWindow.__weaverses[runtime.pageId] = runtime
137
+ runtimeWindow.__weaverse = runtime
138
+ }
139
+
140
+ /**
141
+ * Lifecycle invariant: every item store rendered by a runtime must point back
142
+ * at that same runtime (`item.weaverse === runtime`).
143
+ *
144
+ * Core keeps `Weaverse.itemInstances` process-wide and keyed by item id only,
145
+ * so a client navigation that recreates the runtime under a new request key
146
+ * (e.g. `/` → `/fr-fr`) reuses the existing stores and merely calls
147
+ * `setData()` on them — their `weaverse` back-reference is never updated and
148
+ * keeps pointing at the previous runtime. Anything an item reads through that
149
+ * reference then stays stale: `WeaverseNextItem.getSnapShot()` reads
150
+ * `this.weaverse.translationMap` (locale sidecar), and `Element` /
151
+ * `requestInfo` consumers read the old locale's runtime until a full reload
152
+ * builds clean instances.
153
+ *
154
+ * Rebind only the items belonging to the freshly rendered page — other
155
+ * runtimes co-located on the same document own their own item ids and must
156
+ * keep their bindings.
157
+ */
158
+ function rebindPageItemsToRuntime(runtime: WeaverseNextRuntime) {
159
+ let items = (runtime.data as WeaverseNextPageData | undefined)?.items
160
+ if (!items) {
161
+ return
162
+ }
163
+ for (let { id } of items) {
164
+ let instance = runtime.itemInstances.get(id) as WeaverseNextItem | undefined
165
+ if (instance && instance.weaverse !== runtime) {
166
+ instance.weaverse = runtime
167
+ }
168
+ }
169
+ }
170
+
171
+ /** Weaverse page data with the root component resolved for rendering. */
172
+ export interface WeaverseNextRenderablePage extends WeaverseNextPageData {
173
+ /** Component item ID used as the root of the rendered page tree. */
174
+ rootId: string
175
+ }
176
+
177
+ /**
178
+ * Browser runtime that adapts a Next.js loader payload to `@weaverse/react`
179
+ * and exposes the navigation, settings, and translation state used by Studio.
180
+ */
181
+ export class WeaverseNextRuntime extends Weaverse {
182
+ /** Stable ID of the page owned by this runtime. */
183
+ pageId: string
184
+ /** Mutable callbacks and stores consumed by the Studio bridge. */
185
+ internal: WeaverseNextRuntimeInternal
186
+ /** Path, query, and locale metadata for Studio navigation. */
187
+ requestInfo: WeaverseNextRequestInfo
188
+ /** Whether this runtime renders a component preview. */
189
+ isPreviewMode: boolean
190
+ /** Whether this runtime renders a saved revision. */
191
+ isRevisionPreview: boolean
192
+ /** Component type rendered in section preview mode. */
193
+ sectionType?: string
194
+
195
+ // ─── Item-level translation sidecar (design mode only) ─────────────
196
+ /**
197
+ * Translation map received from Builder in design mode, keyed by item ID.
198
+ * `WeaverseNextItem.getSnapShot()` overlays these onto the data handed to
199
+ * components, so translations render without mutating the base `_store`.
200
+ */
201
+ translationMap: WeaverseNextTranslationMap = {}
202
+ /** Locale associated with the item-level translation sidecar. */
203
+ translationLocale = String()
204
+ /** Weaverse language ID used when saving translation changes. */
205
+ translationLanguageId = String()
206
+
207
+ /**
208
+ * Fresh page data captured while React was rendering. Runtime reuse happens
209
+ * inside the renderer's render phase (`useMemo`), where applying it through
210
+ * `setProjectData()` would emit item updates to `useSyncExternalStore`
211
+ * subscribers mid-render. The renderer applies it post-commit via
212
+ * `flushRenderPhaseUpdates()`. Never set on design-mode runtimes — the live
213
+ * Studio tree owns the page data there.
214
+ */
215
+ pendingProjectData?: WeaverseNextRenderablePage
216
+
217
+ /**
218
+ * Reused item instances whose data was refreshed while this runtime was
219
+ * constructed during render (core `initProject()` calls `setData()` on
220
+ * them). Their data is already fresh; only the subscriber notification was
221
+ * deferred to `flushRenderPhaseUpdates()`.
222
+ */
223
+ pendingItemUpdates: WeaverseNextItem[] = []
224
+
225
+ /** Create a browser runtime from a loaded page and its request context. */
226
+ constructor(config: WeaverseNextRuntimeConfig) {
227
+ let { client, data } = config
228
+ let page = getRenderablePage(data)
229
+ let configs = data.configs
230
+ let requestContext = client?.requestContext
231
+ let isDesignMode = resolveModeFlag(config, 'isDesignMode')
232
+ let isPreviewMode = resolveModeFlag(config, 'isPreviewMode')
233
+ let isRevisionPreview = resolveModeFlag(config, 'isRevisionPreview')
234
+ let sectionType = resolveSectionType(config)
235
+ let projectId = resolveProjectId(client, data, page.id)
236
+
237
+ ensureNextItemConstructor()
238
+ super({
239
+ projectId,
240
+ data: page,
241
+ isDesignMode,
242
+ weaverseHost: getConfigString(configs, 'weaverseHost'),
243
+ weaverseVersion: getConfigString(configs, 'weaverseVersion'),
244
+ })
245
+
246
+ this.pageId = page.id
247
+ this.dataContext = resolveDataContext(config)
248
+ this.requestInfo = buildWeaverseNextRequestInfo(requestContext)
249
+ this.isDesignMode = isDesignMode
250
+ this.isPreviewMode = isPreviewMode
251
+ this.isRevisionPreview = isRevisionPreview
252
+ this.sectionType = sectionType
253
+
254
+ // One store instance backs both the canonical `translationStore` and the
255
+ // deprecated `themeTextStore` alias, so Builder's `updateStaticText()` RPC
256
+ // (which reads `internal.themeTextStore`) mutates the same store the
257
+ // `TranslationProvider` subscribes to.
258
+ let translationStore = config.translationStore ?? new TranslationStore()
259
+
260
+ this.internal = {
261
+ merchantOverrides: config.merchantOverrides,
262
+ navigate: config.navigate,
263
+ pageAssignment: data.pageAssignment,
264
+ project: data.project,
265
+ revalidate: config.revalidate,
266
+ themeSettingsStore:
267
+ config.themeSettingsStore ??
268
+ createWeaverseNextThemeSettingsStore({
269
+ schema: client?.themeSchema,
270
+ settings: client?.themeSettings,
271
+ }),
272
+ studioSelectionStore: config.studioSelectionStore,
273
+ translationStore,
274
+ themeTextStore: translationStore,
275
+ }
276
+
277
+ // Read any item-level translation sidecar the loader attached to the page
278
+ // data. Runs after `super()` has built `this.data` and the item stores.
279
+ this.extractTranslationSidecar()
280
+ }
281
+
282
+ /**
283
+ * Pull `translationMap` / `translationLocale` / `translationLanguageId` off
284
+ * the current page data (Builder attaches them in design mode). Unlike
285
+ * Hydrogen, this also clears the sidecar to defaults when the new page data
286
+ * carries none, so a reused runtime / project-data swap can't leave a stale
287
+ * map overlaying the fresh tree.
288
+ */
289
+ extractTranslationSidecar = () => {
290
+ let pageData = this.data as WeaverseNextPageData | undefined
291
+ let translationMap = pageData?.translationMap as
292
+ | WeaverseNextTranslationMap
293
+ | undefined
294
+ if (translationMap) {
295
+ this.translationMap = translationMap
296
+ this.translationLocale = (pageData?.translationLocale as string) || ''
297
+ this.translationLanguageId =
298
+ (pageData?.translationLanguageId as string) || ''
299
+ } else {
300
+ this.translationMap = {}
301
+ this.translationLocale = ''
302
+ this.translationLanguageId = ''
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Replace the translation sidecar (Builder pushes this when translation data
308
+ * arrives or the active language changes) and refresh every item so their
309
+ * merged snapshots recompute.
310
+ */
311
+ setTranslationSidecar = (
312
+ map: WeaverseNextTranslationMap = {},
313
+ locale = '',
314
+ languageId = ''
315
+ ) => {
316
+ this.translationMap = map
317
+ this.translationLocale = locale
318
+ this.translationLanguageId = languageId
319
+ this.refreshAllItems()
320
+ }
321
+
322
+ /**
323
+ * Update a single translatable field for one item. Assigns a new per-item
324
+ * entry object so `WeaverseNextItem.getSnapShot()`'s translation-ref cache
325
+ * invalidates, then triggers only that item's subscribers.
326
+ */
327
+ updateTranslation = (
328
+ itemId: string,
329
+ key: string,
330
+ originalValue: string,
331
+ translatedValue: string
332
+ ) => {
333
+ if (!this.translationMap[itemId]) {
334
+ this.translationMap[itemId] = {}
335
+ }
336
+ // New entry reference so the item's snapshot cache detects the change.
337
+ this.translationMap[itemId] = {
338
+ ...this.translationMap[itemId],
339
+ [key]: { originalValue, translatedValue },
340
+ }
341
+ let instance = this.itemInstances.get(itemId)
342
+ if (instance) {
343
+ instance.triggerUpdate()
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Collect translation changes for the save flow. Returns `undefined` unless
349
+ * both the locale and language ID are set and at least one entry exists;
350
+ * each entry key is namespaced as `data.<field>` to match Builder.
351
+ */
352
+ getTranslationChanges = (): WeaverseNextTranslationChanges | undefined => {
353
+ let languageId = this.translationLanguageId
354
+ if (!(this.translationLocale && languageId)) {
355
+ return
356
+ }
357
+
358
+ let entries: WeaverseNextTranslationEntry[] = []
359
+ for (let [itemId, fields] of Object.entries(this.translationMap)) {
360
+ for (let [key, entry] of Object.entries(fields)) {
361
+ entries.push({
362
+ itemId,
363
+ key: `data.${key}`,
364
+ originalValue: entry.originalValue,
365
+ translatedValue: entry.translatedValue,
366
+ })
367
+ }
368
+ }
369
+
370
+ if (entries.length === 0) {
371
+ return
372
+ }
373
+ return { languageId, entries }
374
+ }
375
+
376
+ /**
377
+ * Re-extract the translation sidecar whenever project data is replaced (route
378
+ * change / non-design reuse), before rebuilding item stores. Takes the
379
+ * renderable page shape (`rootId` resolved) that `getRenderablePage()` and
380
+ * the reuse branch pass in.
381
+ */
382
+ setProjectData = (data: WeaverseNextRenderablePage) => {
383
+ this.data = data
384
+ this.extractTranslationSidecar()
385
+ this.initProject()
386
+ }
387
+
388
+ /**
389
+ * Apply updates deferred off the render phase: swap in any pending page data
390
+ * captured on runtime reuse, then notify items whose data was refreshed
391
+ * during construction. The renderer calls this from a layout effect right
392
+ * after commit, so subscribers re-render before paint (no content flash) and
393
+ * without React's setState-in-render warning.
394
+ */
395
+ flushRenderPhaseUpdates = () => {
396
+ let pendingData = this.pendingProjectData
397
+ if (pendingData) {
398
+ this.pendingProjectData = undefined
399
+ this.setProjectData(pendingData)
400
+ }
401
+
402
+ let pendingItems = this.pendingItemUpdates
403
+ if (pendingItems.length > 0) {
404
+ this.pendingItemUpdates = []
405
+ for (let item of pendingItems) {
406
+ item.triggerUpdate()
407
+ }
408
+ }
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Create a browser runtime or reuse the runtime already registered for the
414
+ * same page and request. Reuse preserves unsaved Studio state in design mode.
415
+ */
416
+ export function createWeaverseNextRuntime(
417
+ config: WeaverseNextRuntimeConfig
418
+ ): WeaverseNextRuntime {
419
+ let requestInfo = buildWeaverseNextRequestInfo(config.client?.requestContext)
420
+ let page = getRenderablePage(config.data)
421
+ let runtimeWindow = getRuntimeWindow()
422
+ let existing = runtimeWindow?.__weaverses?.[page.id] as
423
+ | StudioBoundRuntime
424
+ | undefined
425
+ let requestKey = getRuntimeKey(page.id, requestInfo)
426
+
427
+ if (existing?.__weaverseNextRequestKey === requestKey) {
428
+ let configs = config.data.configs
429
+ let nextIsDesignMode = resolveModeFlag(config, 'isDesignMode')
430
+ let nextIsPreviewMode = resolveModeFlag(config, 'isPreviewMode')
431
+ let nextIsRevisionPreview = resolveModeFlag(config, 'isRevisionPreview')
432
+ let nextSectionType = resolveSectionType(config)
433
+ // In design mode the live Studio runtime owns the page tree, including
434
+ // unsaved drafts. Reapplying loader `page` data here would clobber those
435
+ // edits, so leave the project data untouched and let
436
+ // `bindWeaverseNextStudioRuntime` push the latest state via `refreshStudio`.
437
+ // Published / preview reuse has no draft state, so fresh data still wins —
438
+ // but this factory runs during React render (the renderer's `useMemo`),
439
+ // and `setProjectData()` → `initProject()` → `setData()` would emit item
440
+ // updates to `useSyncExternalStore` subscribers mid-render (React's
441
+ // setState-in-render warning). Stash the page instead; the renderer
442
+ // applies it post-commit via `flushRenderPhaseUpdates()`.
443
+ if (existing.isDesignMode || nextIsDesignMode) {
444
+ existing.pendingProjectData = undefined
445
+ } else {
446
+ existing.pendingProjectData = page
447
+ }
448
+ // Always capture the latest server payload. In design mode the live tree
449
+ // (`existing.data`) is deliberately left untouched above, so it goes stale
450
+ // after an RSC refresh — `bindWeaverseNextStudioRuntime` must report this
451
+ // fresh payload to `refreshStudio`, not the stale tree, or a revalidation
452
+ // (e.g. resource-picker pick) merges back old per-item `loaderData` and the
453
+ // preview keeps rendering the previous resource. Mirrors Hydrogen, which
454
+ // always passes the fresh loader params to `refreshStudio`.
455
+ existing.__weaverseNextLatestData = page
456
+ existing.dataContext = resolveDataContext(config)
457
+ existing.requestInfo = requestInfo
458
+ existing.projectId = resolveProjectId(config.client, config.data, page.id)
459
+ existing.isDesignMode = nextIsDesignMode
460
+ existing.isPreviewMode = nextIsPreviewMode
461
+ existing.isRevisionPreview = nextIsRevisionPreview
462
+ existing.sectionType = nextSectionType
463
+ existing.weaverseHost =
464
+ getConfigString(configs, 'weaverseHost') ?? existing.weaverseHost
465
+ existing.weaverseVersion =
466
+ getConfigString(configs, 'weaverseVersion') ?? existing.weaverseVersion
467
+ existing.internal.pageAssignment = config.data.pageAssignment
468
+ existing.internal.project = config.data.project
469
+ if (config.navigate) {
470
+ existing.internal.navigate = config.navigate
471
+ }
472
+ if (config.revalidate) {
473
+ existing.internal.revalidate = config.revalidate
474
+ }
475
+ if (config.themeSettingsStore) {
476
+ existing.internal.themeSettingsStore = config.themeSettingsStore
477
+ }
478
+ if (config.studioSelectionStore) {
479
+ existing.internal.studioSelectionStore = config.studioSelectionStore
480
+ }
481
+ if (config.translationStore) {
482
+ existing.internal.translationStore = config.translationStore
483
+ // Keep the deprecated alias pointing at the same adopted instance.
484
+ existing.internal.themeTextStore = config.translationStore
485
+ }
486
+ // Always assign, including `undefined`, so locale/navigation changes can
487
+ // clear previously attached merchant overrides instead of leaking stale
488
+ // static-text values into the next render.
489
+ existing.internal.merchantOverrides = config.merchantOverrides
490
+ registerRuntime(existing)
491
+ return existing
492
+ }
493
+
494
+ // Constructing a fresh runtime also happens during render, and core
495
+ // `initProject()` reuses process-wide item instances holding the same item
496
+ // ids (locale/client navigation recreates the runtime under a new request
497
+ // key) by calling `setData()` on them. Defer those subscriber emits to the
498
+ // renderer's post-commit `flushRenderPhaseUpdates()` as well.
499
+ let [runtime, refreshedItems] = collectDeferredItemUpdates(
500
+ () => new WeaverseNextRuntime(config) as StudioBoundRuntime
501
+ )
502
+ runtime.pendingItemUpdates = refreshedItems
503
+ // Reused stores are still bound to the runtime that built them; rebind before
504
+ // returning so consumers read snapshots through this runtime (see
505
+ // `rebindPageItemsToRuntime`). Rebinding touches no subscriber, so it stays
506
+ // render-phase safe alongside the deferred item updates above.
507
+ rebindPageItemsToRuntime(runtime)
508
+ runtime.__weaverseNextRequestKey = requestKey
509
+ runtime.__weaverseNextLatestData = page
510
+ registerRuntime(runtime)
511
+ return runtime
512
+ }
513
+
514
+ /**
515
+ * Bind a design-mode runtime to the global Builder Studio bridge.
516
+ *
517
+ * @returns `true` when Studio was initialized or refreshed, otherwise `false`.
518
+ */
519
+ export function bindWeaverseNextStudioRuntime(runtime: WeaverseNextRuntime) {
520
+ if (!runtime.isDesignMode) {
521
+ return false
522
+ }
523
+
524
+ let studio = getRuntimeWindow()?.weaverseStudio
525
+ if (!studio) {
526
+ return false
527
+ }
528
+
529
+ let boundRuntime = runtime as StudioBoundRuntime
530
+ // Builder's bridge tracks a single active runtime (`studio.weaverse`). When
531
+ // an already-bound runtime is reused after navigating away and back (Home ->
532
+ // PDP -> Home), the bridge is still on the other URL, and Builder ignores
533
+ // `refreshStudio` for a non-active pageId — Studio would stay disconnected
534
+ // (outline skeleton, publish disabled). Re-init only when the active runtime
535
+ // belongs to a different URL; same-URL co-located runtimes must stay on the
536
+ // refresh path so the SDK does not override Builder's editable-instance
537
+ // choice.
538
+ let activeRuntime = studio.weaverse as WeaverseNextRuntime | undefined
539
+ let activeRequestInfo = activeRuntime?.requestInfo
540
+ let isActiveUrl =
541
+ !activeRequestInfo ||
542
+ (activeRequestInfo.pathname === runtime.requestInfo.pathname &&
543
+ activeRequestInfo.search === runtime.requestInfo.search)
544
+ if (!(boundRuntime.__weaverseNextStudioBound && isActiveUrl)) {
545
+ studio.init?.(runtime)
546
+ boundRuntime.__weaverseNextStudioBound = true
547
+ return true
548
+ }
549
+
550
+ // Report the latest server payload (fresh per-item `loaderData`), not the
551
+ // live tree: Builder's `refreshStudio` merges the editor draft's structural
552
+ // edits back on top and only takes the loader payload from this data.
553
+ studio.refreshStudio?.({
554
+ data: boundRuntime.__weaverseNextLatestData ?? runtime.data,
555
+ pageId: runtime.pageId,
556
+ requestInfo: runtime.requestInfo,
557
+ })
558
+ return true
559
+ }