@weaverse/next 0.1.0-alpha.9 → 1.0.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 (106) hide show
  1. package/README.md +155 -73
  2. package/dist/{chunk-6PMZ77RA.mjs → chunk-LH6AAEIK.mjs} +115 -8
  3. package/dist/chunk-LH6AAEIK.mjs.map +1 -0
  4. package/dist/index.js +180 -32
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +86 -47
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/server.js +321 -91
  9. package/dist/server.js.map +1 -1
  10. package/dist/server.mjs +205 -28
  11. package/dist/server.mjs.map +1 -1
  12. package/dist/types/client.d.ts +16 -0
  13. package/dist/types/client.d.ts.map +1 -0
  14. package/dist/types/hooks.d.ts +20 -0
  15. package/dist/types/hooks.d.ts.map +1 -0
  16. package/dist/types/index.d.ts +34 -0
  17. package/dist/types/index.d.ts.map +1 -0
  18. package/dist/types/item.d.ts +60 -0
  19. package/dist/types/item.d.ts.map +1 -0
  20. package/dist/types/loader.d.ts +25 -0
  21. package/dist/types/loader.d.ts.map +1 -0
  22. package/dist/types/main.d.ts +11 -0
  23. package/dist/types/main.d.ts.map +1 -0
  24. package/dist/types/provider.d.ts +82 -0
  25. package/dist/types/provider.d.ts.map +1 -0
  26. package/dist/types/registry.d.ts +15 -0
  27. package/dist/types/registry.d.ts.map +1 -0
  28. package/dist/types/renderer.d.ts +21 -0
  29. package/dist/types/renderer.d.ts.map +1 -0
  30. package/dist/types/request-info.d.ts +19 -0
  31. package/dist/types/request-info.d.ts.map +1 -0
  32. package/dist/types/revalidate-item.d.ts +82 -0
  33. package/dist/types/revalidate-item.d.ts.map +1 -0
  34. package/dist/types/root-provider.d.ts +62 -0
  35. package/dist/types/root-provider.d.ts.map +1 -0
  36. package/dist/types/runtime.d.ts +113 -0
  37. package/dist/types/runtime.d.ts.map +1 -0
  38. package/dist/types/seo.d.ts +55 -0
  39. package/dist/types/seo.d.ts.map +1 -0
  40. package/dist/types/server/configs.d.ts +27 -0
  41. package/dist/types/server/configs.d.ts.map +1 -0
  42. package/dist/types/server/normalize-page-url.d.ts +19 -0
  43. package/dist/types/server/normalize-page-url.d.ts.map +1 -0
  44. package/dist/types/server/revalidate-handler.d.ts +55 -0
  45. package/dist/types/server/revalidate-handler.d.ts.map +1 -0
  46. package/dist/types/server/server-client.d.ts +21 -0
  47. package/dist/types/server/server-client.d.ts.map +1 -0
  48. package/dist/types/server.d.ts +19 -0
  49. package/dist/types/server.d.ts.map +1 -0
  50. package/dist/types/studio-bridge.d.ts +16 -0
  51. package/dist/types/studio-bridge.d.ts.map +1 -0
  52. package/dist/types/studio-connect.d.ts +27 -0
  53. package/dist/types/studio-connect.d.ts.map +1 -0
  54. package/dist/types/studio-router.d.ts +50 -0
  55. package/dist/types/studio-router.d.ts.map +1 -0
  56. package/dist/types/studio-script-src.d.ts +12 -0
  57. package/dist/types/studio-script-src.d.ts.map +1 -0
  58. package/dist/types/theme-settings-store.d.ts +18 -0
  59. package/dist/types/theme-settings-store.d.ts.map +1 -0
  60. package/dist/types/translation-context.d.ts +156 -0
  61. package/dist/types/translation-context.d.ts.map +1 -0
  62. package/dist/types/translation-store.d.ts +44 -0
  63. package/dist/types/translation-store.d.ts.map +1 -0
  64. package/dist/types/types.d.ts +665 -0
  65. package/dist/types/types.d.ts.map +1 -0
  66. package/dist/types/use-weaverse-next-studio.d.ts +34 -0
  67. package/dist/types/use-weaverse-next-studio.d.ts.map +1 -0
  68. package/dist/types/utils.d.ts +12 -0
  69. package/dist/types/utils.d.ts.map +1 -0
  70. package/package.json +12 -8
  71. package/src/client.ts +102 -0
  72. package/src/hooks.ts +50 -0
  73. package/src/index.ts +153 -0
  74. package/src/item.ts +167 -0
  75. package/src/loader.ts +135 -0
  76. package/src/main.tsx +45 -0
  77. package/src/provider.tsx +225 -0
  78. package/src/registry.ts +63 -0
  79. package/src/renderer.tsx +100 -0
  80. package/src/request-info.ts +124 -0
  81. package/src/revalidate-item.ts +211 -0
  82. package/src/root-provider.tsx +182 -0
  83. package/src/runtime.ts +555 -0
  84. package/src/seo.ts +157 -0
  85. package/src/server/configs.ts +178 -0
  86. package/src/server/normalize-page-url.ts +73 -0
  87. package/src/server/revalidate-handler.ts +357 -0
  88. package/src/server/server-client.ts +728 -0
  89. package/src/server.ts +67 -0
  90. package/src/studio-bridge.tsx +82 -0
  91. package/src/studio-connect.tsx +88 -0
  92. package/src/studio-router.ts +85 -0
  93. package/src/studio-script-src.ts +128 -0
  94. package/src/theme-settings-store.ts +47 -0
  95. package/src/translation-context.tsx +299 -0
  96. package/src/translation-store.ts +69 -0
  97. package/src/types.ts +753 -0
  98. package/src/use-weaverse-next-studio.tsx +106 -0
  99. package/src/utils.ts +37 -0
  100. package/dist/chunk-6PMZ77RA.mjs.map +0 -1
  101. package/dist/index.d.mts +0 -580
  102. package/dist/index.d.ts +0 -580
  103. package/dist/seo-Dos9t22_.d.mts +0 -477
  104. package/dist/seo-Dos9t22_.d.ts +0 -477
  105. package/dist/server.d.mts +0 -99
  106. package/dist/server.d.ts +0 -99
package/src/runtime.ts ADDED
@@ -0,0 +1,555 @@
1
+ import { Weaverse } from '@weaverse/react'
2
+ import {
3
+ collectDeferredItemUpdates,
4
+ ensureNextItemConstructor,
5
+ type WeaverseNextItem,
6
+ } from './item'
7
+ import { buildWeaverseNextRequestInfo } from './request-info'
8
+ import { createWeaverseNextThemeSettingsStore } from './theme-settings-store'
9
+ import { TranslationStore } from './translation-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
+ translationStore,
273
+ themeTextStore: translationStore,
274
+ }
275
+
276
+ // Read any item-level translation sidecar the loader attached to the page
277
+ // data. Runs after `super()` has built `this.data` and the item stores.
278
+ this.extractTranslationSidecar()
279
+ }
280
+
281
+ /**
282
+ * Pull `translationMap` / `translationLocale` / `translationLanguageId` off
283
+ * the current page data (Builder attaches them in design mode). Unlike
284
+ * Hydrogen, this also clears the sidecar to defaults when the new page data
285
+ * carries none, so a reused runtime / project-data swap can't leave a stale
286
+ * map overlaying the fresh tree.
287
+ */
288
+ extractTranslationSidecar = () => {
289
+ let pageData = this.data as WeaverseNextPageData | undefined
290
+ let translationMap = pageData?.translationMap as
291
+ | WeaverseNextTranslationMap
292
+ | undefined
293
+ if (translationMap) {
294
+ this.translationMap = translationMap
295
+ this.translationLocale = (pageData?.translationLocale as string) || ''
296
+ this.translationLanguageId =
297
+ (pageData?.translationLanguageId as string) || ''
298
+ } else {
299
+ this.translationMap = {}
300
+ this.translationLocale = ''
301
+ this.translationLanguageId = ''
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Replace the translation sidecar (Builder pushes this when translation data
307
+ * arrives or the active language changes) and refresh every item so their
308
+ * merged snapshots recompute.
309
+ */
310
+ setTranslationSidecar = (
311
+ map: WeaverseNextTranslationMap = {},
312
+ locale = '',
313
+ languageId = ''
314
+ ) => {
315
+ this.translationMap = map
316
+ this.translationLocale = locale
317
+ this.translationLanguageId = languageId
318
+ this.refreshAllItems()
319
+ }
320
+
321
+ /**
322
+ * Update a single translatable field for one item. Assigns a new per-item
323
+ * entry object so `WeaverseNextItem.getSnapShot()`'s translation-ref cache
324
+ * invalidates, then triggers only that item's subscribers.
325
+ */
326
+ updateTranslation = (
327
+ itemId: string,
328
+ key: string,
329
+ originalValue: string,
330
+ translatedValue: string
331
+ ) => {
332
+ if (!this.translationMap[itemId]) {
333
+ this.translationMap[itemId] = {}
334
+ }
335
+ // New entry reference so the item's snapshot cache detects the change.
336
+ this.translationMap[itemId] = {
337
+ ...this.translationMap[itemId],
338
+ [key]: { originalValue, translatedValue },
339
+ }
340
+ let instance = this.itemInstances.get(itemId)
341
+ if (instance) {
342
+ instance.triggerUpdate()
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Collect translation changes for the save flow. Returns `undefined` unless
348
+ * both the locale and language ID are set and at least one entry exists;
349
+ * each entry key is namespaced as `data.<field>` to match Builder.
350
+ */
351
+ getTranslationChanges = (): WeaverseNextTranslationChanges | undefined => {
352
+ let languageId = this.translationLanguageId
353
+ if (!(this.translationLocale && languageId)) {
354
+ return
355
+ }
356
+
357
+ let entries: WeaverseNextTranslationEntry[] = []
358
+ for (let [itemId, fields] of Object.entries(this.translationMap)) {
359
+ for (let [key, entry] of Object.entries(fields)) {
360
+ entries.push({
361
+ itemId,
362
+ key: `data.${key}`,
363
+ originalValue: entry.originalValue,
364
+ translatedValue: entry.translatedValue,
365
+ })
366
+ }
367
+ }
368
+
369
+ if (entries.length === 0) {
370
+ return
371
+ }
372
+ return { languageId, entries }
373
+ }
374
+
375
+ /**
376
+ * Re-extract the translation sidecar whenever project data is replaced (route
377
+ * change / non-design reuse), before rebuilding item stores. Takes the
378
+ * renderable page shape (`rootId` resolved) that `getRenderablePage()` and
379
+ * the reuse branch pass in.
380
+ */
381
+ setProjectData = (data: WeaverseNextRenderablePage) => {
382
+ this.data = data
383
+ this.extractTranslationSidecar()
384
+ this.initProject()
385
+ }
386
+
387
+ /**
388
+ * Apply updates deferred off the render phase: swap in any pending page data
389
+ * captured on runtime reuse, then notify items whose data was refreshed
390
+ * during construction. The renderer calls this from a layout effect right
391
+ * after commit, so subscribers re-render before paint (no content flash) and
392
+ * without React's setState-in-render warning.
393
+ */
394
+ flushRenderPhaseUpdates = () => {
395
+ let pendingData = this.pendingProjectData
396
+ if (pendingData) {
397
+ this.pendingProjectData = undefined
398
+ this.setProjectData(pendingData)
399
+ }
400
+
401
+ let pendingItems = this.pendingItemUpdates
402
+ if (pendingItems.length > 0) {
403
+ this.pendingItemUpdates = []
404
+ for (let item of pendingItems) {
405
+ item.triggerUpdate()
406
+ }
407
+ }
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Create a browser runtime or reuse the runtime already registered for the
413
+ * same page and request. Reuse preserves unsaved Studio state in design mode.
414
+ */
415
+ export function createWeaverseNextRuntime(
416
+ config: WeaverseNextRuntimeConfig
417
+ ): WeaverseNextRuntime {
418
+ let requestInfo = buildWeaverseNextRequestInfo(config.client?.requestContext)
419
+ let page = getRenderablePage(config.data)
420
+ let runtimeWindow = getRuntimeWindow()
421
+ let existing = runtimeWindow?.__weaverses?.[page.id] as
422
+ | StudioBoundRuntime
423
+ | undefined
424
+ let requestKey = getRuntimeKey(page.id, requestInfo)
425
+
426
+ if (existing?.__weaverseNextRequestKey === requestKey) {
427
+ let configs = config.data.configs
428
+ let nextIsDesignMode = resolveModeFlag(config, 'isDesignMode')
429
+ let nextIsPreviewMode = resolveModeFlag(config, 'isPreviewMode')
430
+ let nextIsRevisionPreview = resolveModeFlag(config, 'isRevisionPreview')
431
+ let nextSectionType = resolveSectionType(config)
432
+ // In design mode the live Studio runtime owns the page tree, including
433
+ // unsaved drafts. Reapplying loader `page` data here would clobber those
434
+ // edits, so leave the project data untouched and let
435
+ // `bindWeaverseNextStudioRuntime` push the latest state via `refreshStudio`.
436
+ // Published / preview reuse has no draft state, so fresh data still wins —
437
+ // but this factory runs during React render (the renderer's `useMemo`),
438
+ // and `setProjectData()` → `initProject()` → `setData()` would emit item
439
+ // updates to `useSyncExternalStore` subscribers mid-render (React's
440
+ // setState-in-render warning). Stash the page instead; the renderer
441
+ // applies it post-commit via `flushRenderPhaseUpdates()`.
442
+ if (existing.isDesignMode || nextIsDesignMode) {
443
+ existing.pendingProjectData = undefined
444
+ } else {
445
+ existing.pendingProjectData = page
446
+ }
447
+ // Always capture the latest server payload. In design mode the live tree
448
+ // (`existing.data`) is deliberately left untouched above, so it goes stale
449
+ // after an RSC refresh — `bindWeaverseNextStudioRuntime` must report this
450
+ // fresh payload to `refreshStudio`, not the stale tree, or a revalidation
451
+ // (e.g. resource-picker pick) merges back old per-item `loaderData` and the
452
+ // preview keeps rendering the previous resource. Mirrors Hydrogen, which
453
+ // always passes the fresh loader params to `refreshStudio`.
454
+ existing.__weaverseNextLatestData = page
455
+ existing.dataContext = resolveDataContext(config)
456
+ existing.requestInfo = requestInfo
457
+ existing.projectId = resolveProjectId(config.client, config.data, page.id)
458
+ existing.isDesignMode = nextIsDesignMode
459
+ existing.isPreviewMode = nextIsPreviewMode
460
+ existing.isRevisionPreview = nextIsRevisionPreview
461
+ existing.sectionType = nextSectionType
462
+ existing.weaverseHost =
463
+ getConfigString(configs, 'weaverseHost') ?? existing.weaverseHost
464
+ existing.weaverseVersion =
465
+ getConfigString(configs, 'weaverseVersion') ?? existing.weaverseVersion
466
+ existing.internal.pageAssignment = config.data.pageAssignment
467
+ existing.internal.project = config.data.project
468
+ if (config.navigate) {
469
+ existing.internal.navigate = config.navigate
470
+ }
471
+ if (config.revalidate) {
472
+ existing.internal.revalidate = config.revalidate
473
+ }
474
+ if (config.themeSettingsStore) {
475
+ existing.internal.themeSettingsStore = config.themeSettingsStore
476
+ }
477
+ if (config.translationStore) {
478
+ existing.internal.translationStore = config.translationStore
479
+ // Keep the deprecated alias pointing at the same adopted instance.
480
+ existing.internal.themeTextStore = config.translationStore
481
+ }
482
+ // Always assign, including `undefined`, so locale/navigation changes can
483
+ // clear previously attached merchant overrides instead of leaking stale
484
+ // static-text values into the next render.
485
+ existing.internal.merchantOverrides = config.merchantOverrides
486
+ registerRuntime(existing)
487
+ return existing
488
+ }
489
+
490
+ // Constructing a fresh runtime also happens during render, and core
491
+ // `initProject()` reuses process-wide item instances holding the same item
492
+ // ids (locale/client navigation recreates the runtime under a new request
493
+ // key) by calling `setData()` on them. Defer those subscriber emits to the
494
+ // renderer's post-commit `flushRenderPhaseUpdates()` as well.
495
+ let [runtime, refreshedItems] = collectDeferredItemUpdates(
496
+ () => new WeaverseNextRuntime(config) as StudioBoundRuntime
497
+ )
498
+ runtime.pendingItemUpdates = refreshedItems
499
+ // Reused stores are still bound to the runtime that built them; rebind before
500
+ // returning so consumers read snapshots through this runtime (see
501
+ // `rebindPageItemsToRuntime`). Rebinding touches no subscriber, so it stays
502
+ // render-phase safe alongside the deferred item updates above.
503
+ rebindPageItemsToRuntime(runtime)
504
+ runtime.__weaverseNextRequestKey = requestKey
505
+ runtime.__weaverseNextLatestData = page
506
+ registerRuntime(runtime)
507
+ return runtime
508
+ }
509
+
510
+ /**
511
+ * Bind a design-mode runtime to the global Builder Studio bridge.
512
+ *
513
+ * @returns `true` when Studio was initialized or refreshed, otherwise `false`.
514
+ */
515
+ export function bindWeaverseNextStudioRuntime(runtime: WeaverseNextRuntime) {
516
+ if (!runtime.isDesignMode) {
517
+ return false
518
+ }
519
+
520
+ let studio = getRuntimeWindow()?.weaverseStudio
521
+ if (!studio) {
522
+ return false
523
+ }
524
+
525
+ let boundRuntime = runtime as StudioBoundRuntime
526
+ // Builder's bridge tracks a single active runtime (`studio.weaverse`). When
527
+ // an already-bound runtime is reused after navigating away and back (Home ->
528
+ // PDP -> Home), the bridge is still on the other URL, and Builder ignores
529
+ // `refreshStudio` for a non-active pageId — Studio would stay disconnected
530
+ // (outline skeleton, publish disabled). Re-init only when the active runtime
531
+ // belongs to a different URL; same-URL co-located runtimes must stay on the
532
+ // refresh path so the SDK does not override Builder's editable-instance
533
+ // choice.
534
+ let activeRuntime = studio.weaverse as WeaverseNextRuntime | undefined
535
+ let activeRequestInfo = activeRuntime?.requestInfo
536
+ let isActiveUrl =
537
+ !activeRequestInfo ||
538
+ (activeRequestInfo.pathname === runtime.requestInfo.pathname &&
539
+ activeRequestInfo.search === runtime.requestInfo.search)
540
+ if (!(boundRuntime.__weaverseNextStudioBound && isActiveUrl)) {
541
+ studio.init?.(runtime)
542
+ boundRuntime.__weaverseNextStudioBound = true
543
+ return true
544
+ }
545
+
546
+ // Report the latest server payload (fresh per-item `loaderData`), not the
547
+ // live tree: Builder's `refreshStudio` merges the editor draft's structural
548
+ // edits back on top and only takes the loader payload from this data.
549
+ studio.refreshStudio?.({
550
+ data: boundRuntime.__weaverseNextLatestData ?? runtime.data,
551
+ pageId: runtime.pageId,
552
+ requestInfo: runtime.requestInfo,
553
+ })
554
+ return true
555
+ }