@weaverse/next 0.1.0-alpha.9 → 1.0.1

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 (116) hide show
  1. package/README.md +161 -73
  2. package/dist/{chunk-6PMZ77RA.mjs → chunk-OSC5HR5V.mjs} +117 -8
  3. package/dist/chunk-OSC5HR5V.mjs.map +1 -0
  4. package/dist/client-boundary.js +1425 -0
  5. package/dist/client-boundary.js.map +1 -0
  6. package/dist/client-boundary.mjs +1395 -0
  7. package/dist/client-boundary.mjs.map +1 -0
  8. package/dist/index.js +292 -1003
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +90 -966
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/server.js +321 -91
  13. package/dist/server.js.map +1 -1
  14. package/dist/server.mjs +205 -28
  15. package/dist/server.mjs.map +1 -1
  16. package/dist/types/client-boundary.d.ts +25 -0
  17. package/dist/types/client-boundary.d.ts.map +1 -0
  18. package/dist/types/client.d.ts +16 -0
  19. package/dist/types/client.d.ts.map +1 -0
  20. package/dist/types/hooks.d.ts +20 -0
  21. package/dist/types/hooks.d.ts.map +1 -0
  22. package/dist/types/index.d.ts +33 -0
  23. package/dist/types/index.d.ts.map +1 -0
  24. package/dist/types/item.d.ts +60 -0
  25. package/dist/types/item.d.ts.map +1 -0
  26. package/dist/types/loader.d.ts +25 -0
  27. package/dist/types/loader.d.ts.map +1 -0
  28. package/dist/types/main.d.ts +11 -0
  29. package/dist/types/main.d.ts.map +1 -0
  30. package/dist/types/provider.d.ts +82 -0
  31. package/dist/types/provider.d.ts.map +1 -0
  32. package/dist/types/registry.d.ts +15 -0
  33. package/dist/types/registry.d.ts.map +1 -0
  34. package/dist/types/renderer.d.ts +21 -0
  35. package/dist/types/renderer.d.ts.map +1 -0
  36. package/dist/types/request-info.d.ts +19 -0
  37. package/dist/types/request-info.d.ts.map +1 -0
  38. package/dist/types/revalidate-item.d.ts +82 -0
  39. package/dist/types/revalidate-item.d.ts.map +1 -0
  40. package/dist/types/root-provider.d.ts +62 -0
  41. package/dist/types/root-provider.d.ts.map +1 -0
  42. package/dist/types/runtime.d.ts +113 -0
  43. package/dist/types/runtime.d.ts.map +1 -0
  44. package/dist/types/seo.d.ts +55 -0
  45. package/dist/types/seo.d.ts.map +1 -0
  46. package/dist/types/server/configs.d.ts +27 -0
  47. package/dist/types/server/configs.d.ts.map +1 -0
  48. package/dist/types/server/normalize-page-url.d.ts +19 -0
  49. package/dist/types/server/normalize-page-url.d.ts.map +1 -0
  50. package/dist/types/server/revalidate-handler.d.ts +55 -0
  51. package/dist/types/server/revalidate-handler.d.ts.map +1 -0
  52. package/dist/types/server/server-client.d.ts +21 -0
  53. package/dist/types/server/server-client.d.ts.map +1 -0
  54. package/dist/types/server.d.ts +19 -0
  55. package/dist/types/server.d.ts.map +1 -0
  56. package/dist/types/studio-bridge.d.ts +16 -0
  57. package/dist/types/studio-bridge.d.ts.map +1 -0
  58. package/dist/types/studio-connect.d.ts +27 -0
  59. package/dist/types/studio-connect.d.ts.map +1 -0
  60. package/dist/types/studio-router.d.ts +50 -0
  61. package/dist/types/studio-router.d.ts.map +1 -0
  62. package/dist/types/studio-script-src.d.ts +12 -0
  63. package/dist/types/studio-script-src.d.ts.map +1 -0
  64. package/dist/types/theme-settings-store.d.ts +18 -0
  65. package/dist/types/theme-settings-store.d.ts.map +1 -0
  66. package/dist/types/translate.d.ts +56 -0
  67. package/dist/types/translate.d.ts.map +1 -0
  68. package/dist/types/translation-context.d.ts +109 -0
  69. package/dist/types/translation-context.d.ts.map +1 -0
  70. package/dist/types/translation-store.d.ts +44 -0
  71. package/dist/types/translation-store.d.ts.map +1 -0
  72. package/dist/types/types.d.ts +665 -0
  73. package/dist/types/types.d.ts.map +1 -0
  74. package/dist/types/use-weaverse-next-studio.d.ts +34 -0
  75. package/dist/types/use-weaverse-next-studio.d.ts.map +1 -0
  76. package/dist/types/utils.d.ts +22 -0
  77. package/dist/types/utils.d.ts.map +1 -0
  78. package/package.json +12 -27
  79. package/src/client-boundary.ts +50 -0
  80. package/src/client.ts +102 -0
  81. package/src/hooks.ts +50 -0
  82. package/src/index.ts +143 -0
  83. package/src/item.ts +167 -0
  84. package/src/loader.ts +135 -0
  85. package/src/main.tsx +45 -0
  86. package/src/provider.tsx +223 -0
  87. package/src/registry.ts +63 -0
  88. package/src/renderer.tsx +100 -0
  89. package/src/request-info.ts +124 -0
  90. package/src/revalidate-item.ts +211 -0
  91. package/src/root-provider.tsx +180 -0
  92. package/src/runtime.ts +555 -0
  93. package/src/seo.ts +157 -0
  94. package/src/server/configs.ts +178 -0
  95. package/src/server/normalize-page-url.ts +73 -0
  96. package/src/server/revalidate-handler.ts +357 -0
  97. package/src/server/server-client.ts +728 -0
  98. package/src/server.ts +67 -0
  99. package/src/studio-bridge.tsx +82 -0
  100. package/src/studio-connect.tsx +88 -0
  101. package/src/studio-router.ts +85 -0
  102. package/src/studio-script-src.ts +128 -0
  103. package/src/theme-settings-store.ts +47 -0
  104. package/src/translate.ts +125 -0
  105. package/src/translation-context.tsx +182 -0
  106. package/src/translation-store.ts +69 -0
  107. package/src/types.ts +753 -0
  108. package/src/use-weaverse-next-studio.tsx +106 -0
  109. package/src/utils.ts +49 -0
  110. package/dist/chunk-6PMZ77RA.mjs.map +0 -1
  111. package/dist/index.d.mts +0 -580
  112. package/dist/index.d.ts +0 -580
  113. package/dist/seo-Dos9t22_.d.mts +0 -477
  114. package/dist/seo-Dos9t22_.d.ts +0 -477
  115. package/dist/server.d.mts +0 -99
  116. package/dist/server.d.ts +0 -99
package/src/item.ts ADDED
@@ -0,0 +1,167 @@
1
+ import type { ElementData } from '@weaverse/react'
2
+ import { Weaverse, WeaverseItemStore } from '@weaverse/react'
3
+ import type { WeaverseNextRuntime } from './runtime'
4
+ import type {
5
+ WeaverseNextComponentData,
6
+ WeaverseNextTranslationItemEntry,
7
+ } from './types'
8
+ import { generateDataFromSchema } from './utils'
9
+
10
+ /**
11
+ * Spread a serialized item's nested `data` field into top-level props — the
12
+ * shape the shared `@weaverse/react` renderer expects. Top-level fields win
13
+ * over nested ones, matching the constructor's original
14
+ * `Object.assign(store, schemaDefaults, data, rest)` merge order. Flat payloads
15
+ * with no nested `data` pass through unchanged.
16
+ */
17
+ function flattenItemData(update: Omit<ElementData, 'id' | 'type'>) {
18
+ let { data, ...rest } = update
19
+ if (!(data && typeof data === 'object' && !Array.isArray(data))) {
20
+ return rest
21
+ }
22
+ return { ...data, ...rest }
23
+ }
24
+
25
+ /**
26
+ * While set, item `setData()` queues its subscriber notification here instead
27
+ * of emitting synchronously. `createWeaverseNextRuntime()` runs during React
28
+ * render (the renderer's `useMemo`), and constructing a runtime makes core's
29
+ * `initProject()` call `setData()` on reused item instances (locale/client
30
+ * navigation) — emitting to `useSyncExternalStore` subscribers mid-render
31
+ * trips React's setState-in-render warning. Data is still applied
32
+ * synchronously; only the emit is deferred.
33
+ */
34
+ let deferredItemUpdates: Set<WeaverseNextItem> | null = null
35
+
36
+ /**
37
+ * Run `create` with item update emissions deferred, returning its result and
38
+ * the items whose `setData()` fired meanwhile. The caller is responsible for
39
+ * calling `triggerUpdate()` on them once React has committed.
40
+ */
41
+ export function collectDeferredItemUpdates<T>(
42
+ create: () => T
43
+ ): [T, WeaverseNextItem[]] {
44
+ let previousQueue = deferredItemUpdates
45
+ let queue = new Set<WeaverseNextItem>()
46
+ deferredItemUpdates = queue
47
+ try {
48
+ return [create(), [...queue]]
49
+ } finally {
50
+ deferredItemUpdates = previousQueue
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Item store that flattens the serialized item's nested `data` field onto the
56
+ * store and seeds schema defaults — same shape Hydrogen produces, so the
57
+ * shared `@weaverse/react` renderer can spread props at the top level.
58
+ */
59
+ export class WeaverseNextItem extends WeaverseItemStore {
60
+ /** Browser runtime that owns this item store. */
61
+ declare weaverse: WeaverseNextRuntime
62
+
63
+ /**
64
+ * Cached merged snapshot (base `_store` + translations). Reset whenever the
65
+ * `_store` ref or the item's translation entry ref changes, so
66
+ * `useSyncExternalStore` sees a stable object between unrelated re-renders.
67
+ */
68
+ private _cachedSnapshot: ElementData | null = null
69
+ private _snapshotStoreRef: ElementData | null = null
70
+ private _snapshotTranslationRef: WeaverseNextTranslationItemEntry | null =
71
+ null
72
+
73
+ /** Create an item store from serialized component data and its runtime. */
74
+ constructor(initialData: WeaverseNextComponentData, weaverse: Weaverse) {
75
+ super(initialData as ElementData, weaverse)
76
+ let schemaData = generateDataFromSchema(this.Element?.schema)
77
+ Object.assign(this._store, schemaData, flattenItemData(initialData))
78
+ }
79
+
80
+ /**
81
+ * Flatten nested `data` on updates exactly like the constructor does. Core's
82
+ * `initProject()` calls `setData(item)` when a new runtime serves a page
83
+ * whose item instances already exist (e.g. locale/client navigation).
84
+ *
85
+ * A complete serialized item (it carries `id` and `type`; core's
86
+ * `initProject()` always passes one) replaces the store the way the
87
+ * constructor builds it: the schema defaults of the item's own `type` — the
88
+ * Builder can replace a section in place, keeping its id — then the item's
89
+ * settings. A payload omits a setting — or the whole optional `data` — when
90
+ * its value equals the schema default, so merging into the previous store
91
+ * would keep the previous locale's value visible. Partial updates still
92
+ * merge. Either way the `_store` ref changes, which invalidates the memoized
93
+ * snapshot below. The subscriber emit is queued instead of fired when a
94
+ * render-phase deferral is active (see `collectDeferredItemUpdates`).
95
+ */
96
+ setData = (update: Omit<ElementData, 'id' | 'type'>) => {
97
+ if ('id' in update && 'type' in update) {
98
+ let { data, type } = update as ElementData
99
+ let schema = this.weaverse.elementRegistry.get(type)?.schema
100
+ this._store = {
101
+ ...generateDataFromSchema(schema),
102
+ ...(data === undefined ? {} : { data }),
103
+ ...flattenItemData(update),
104
+ } as ElementData
105
+ } else {
106
+ this.data = flattenItemData(update)
107
+ }
108
+ if (deferredItemUpdates) {
109
+ deferredItemUpdates.add(this)
110
+ } else {
111
+ this.triggerUpdate()
112
+ }
113
+ return this.data
114
+ }
115
+
116
+ /**
117
+ * Overlay item-level translations onto the base store when the runtime is in
118
+ * translation mode. Mirrors Hydrogen's `WeaverseHydrogenItem.getSnapShot()`:
119
+ * memoized by `_store` ref + translation entry ref so the returned object is
120
+ * referentially stable, and a fast path that returns the base store untouched
121
+ * when the item has no translations.
122
+ */
123
+ getSnapShot = (): ElementData => {
124
+ let base = this._store
125
+ let translations = this.weaverse.translationMap[this._id]
126
+
127
+ // No translations → return the base store directly (fast path).
128
+ if (!translations) {
129
+ return base
130
+ }
131
+
132
+ // Reuse the cached merge while both the store and translation refs hold.
133
+ if (
134
+ this._cachedSnapshot &&
135
+ this._snapshotStoreRef === base &&
136
+ this._snapshotTranslationRef === translations
137
+ ) {
138
+ return this._cachedSnapshot
139
+ }
140
+
141
+ let merged = { ...base }
142
+ for (let [key, entry] of Object.entries(translations)) {
143
+ if (entry.translatedValue !== undefined) {
144
+ merged[key] = entry.translatedValue
145
+ }
146
+ }
147
+
148
+ this._cachedSnapshot = merged
149
+ this._snapshotStoreRef = base
150
+ this._snapshotTranslationRef = translations
151
+ return merged
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Register the Next item constructor globally. Idempotent: calling it more than
157
+ * once is safe. Invoked by the renderer before it builds a Weaverse instance.
158
+ */
159
+ export function ensureNextItemConstructor() {
160
+ if (
161
+ !Weaverse.ItemConstructor ||
162
+ Weaverse.ItemConstructor === WeaverseItemStore ||
163
+ Weaverse.ItemConstructor === WeaverseNextItem
164
+ ) {
165
+ Weaverse.ItemConstructor = WeaverseNextItem
166
+ }
167
+ }
package/src/loader.ts ADDED
@@ -0,0 +1,135 @@
1
+ import type {
2
+ WeaverseNextClient,
3
+ WeaverseNextCommerceContext,
4
+ WeaverseNextComponent,
5
+ WeaverseNextComponentData,
6
+ WeaverseNextLoaderData,
7
+ WeaverseNextRequestContext,
8
+ } from './types'
9
+ import { generateDataFromSchema } from './utils'
10
+
11
+ /** Inputs for running registered component loaders over a page tree. */
12
+ export interface RunComponentLoadersArgs {
13
+ /** Weaverse client containing the component registry. */
14
+ client: WeaverseNextClient
15
+ /** Request-scoped commerce override; defaults to `client.commerce`. */
16
+ commerce?: WeaverseNextCommerceContext
17
+ /** Request-context override; defaults to `client.requestContext`. */
18
+ context?: WeaverseNextRequestContext
19
+ /** Page payload to walk; defaults to `client.data`. */
20
+ data?: WeaverseNextLoaderData | null
21
+ }
22
+
23
+ function isItemNode(node: unknown): node is WeaverseNextComponentData {
24
+ return (
25
+ typeof node === 'object' &&
26
+ node !== null &&
27
+ typeof (node as WeaverseNextComponentData).type === 'string'
28
+ )
29
+ }
30
+
31
+ function cloneItem(item: WeaverseNextComponentData): WeaverseNextComponentData {
32
+ return {
33
+ ...item,
34
+ children: Array.isArray(item.children)
35
+ ? item.children.map((child) =>
36
+ isItemNode(child) ? cloneItem(child) : { ...child }
37
+ )
38
+ : item.children,
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Walk a serialized page tree and run each registered component's `loader`,
44
+ * attaching the result to the item as `loaderData`.
45
+ *
46
+ * Loaders receive explicit `commerce.storefront` (and `weaverse.storefront` as
47
+ * a compatibility alias via the client). Children are walked recursively when
48
+ * they are inline item objects; flat `items` arrays are covered by the
49
+ * top-level walk.
50
+ *
51
+ * @returns a cloned `data` object with `loaderData` attached to cloned items.
52
+ */
53
+ export async function runWeaverseComponentLoaders(
54
+ args: RunComponentLoadersArgs
55
+ ): Promise<WeaverseNextLoaderData | null> {
56
+ let { client, context, commerce } = args
57
+ let sourceData = args.data ?? client.data
58
+ if (!sourceData) {
59
+ return null
60
+ }
61
+ if (!sourceData.page.items.length) {
62
+ return sourceData
63
+ }
64
+
65
+ let data: WeaverseNextLoaderData = {
66
+ ...sourceData,
67
+ page: {
68
+ ...sourceData.page,
69
+ items: sourceData.page.items.map(cloneItem),
70
+ },
71
+ }
72
+ let items = data.page.items
73
+
74
+ let componentMap = new Map<string, WeaverseNextComponent>()
75
+ for (let component of client.components) {
76
+ if (component?.schema?.type) {
77
+ componentMap.set(component.schema.type, component)
78
+ }
79
+ }
80
+
81
+ let visited = new Set<string>()
82
+ let resolvedContext = context ?? client.requestContext
83
+ let resolvedCommerce = commerce ?? client.commerce
84
+
85
+ // Per-call `weaverse` alias. When a request-scoped `commerce` is passed, the
86
+ // `weaverse.storefront` compatibility alias must resolve to *that* storefront,
87
+ // not the client's construction-time commerce. Pilot-style loaders call
88
+ // `weaverse.storefront.query(...)`, so a stale alias would query the wrong
89
+ // (or an undefined) storefront. When no override is given we reuse the client
90
+ // as-is to preserve its prototype getters.
91
+ let resolvedWeaverse: WeaverseNextClient =
92
+ resolvedCommerce === client.commerce
93
+ ? client
94
+ : {
95
+ ...client,
96
+ commerce: resolvedCommerce,
97
+ storefront: resolvedCommerce?.storefront,
98
+ }
99
+
100
+ async function walk(nodes: WeaverseNextComponentData[]) {
101
+ for (let item of nodes) {
102
+ if (!item || visited.has(item.id)) {
103
+ continue
104
+ }
105
+ visited.add(item.id)
106
+
107
+ let component = componentMap.get(item.type)
108
+ if (component?.loader) {
109
+ try {
110
+ item.loaderData = await component.loader({
111
+ data: { ...generateDataFromSchema(component.schema), ...item.data },
112
+ weaverse: resolvedWeaverse,
113
+ context: resolvedContext,
114
+ commerce: resolvedCommerce,
115
+ })
116
+ } catch (error) {
117
+ console.warn('❌ Item loader run failed.', item.type, item.id, error)
118
+ }
119
+ }
120
+
121
+ let children = item.children
122
+ if (Array.isArray(children)) {
123
+ let inlineChildren = children.filter(
124
+ isItemNode
125
+ ) as WeaverseNextComponentData[]
126
+ if (inlineChildren.length) {
127
+ await walk(inlineChildren)
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ await walk(items)
134
+ return data
135
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,45 @@
1
+ import { createSchema, type SchemaType } from '@weaverse/schema'
2
+ import type { HTMLAttributes, RefObject } from 'react'
3
+ import type { WeaverseNextComponent, WeaverseNextComponentProps } from './types'
4
+
5
+ /**
6
+ * Default `main` root component.
7
+ *
8
+ * A Weaverse page tree is a flat `items` array with a single root item of type
9
+ * `main` whose `children` are the page's top-level sections. Hydrogen ships its
10
+ * own `main`; the Next adapter provides this minimal, framework-neutral
11
+ * equivalent so consumers don't have to register one just to render a basic
12
+ * page tree (omitting it would leave child sections unrendered).
13
+ *
14
+ * It renders `children` and forwards the Weaverse DOM attributes the shared
15
+ * renderer injects (`data-wv-id`, `data-wv-type`, `ref`, `className`, ...rest)
16
+ * onto the root element. `loaderData` is adapter-internal and stripped so it
17
+ * never lands on the DOM node.
18
+ */
19
+ function Main(
20
+ props: WeaverseNextComponentProps & {
21
+ ref?: RefObject<HTMLDivElement | null>
22
+ }
23
+ ) {
24
+ let { children, loaderData, ...rest } = props
25
+ // `rest` carries the Weaverse-injected DOM attributes (data-wv-*, ref,
26
+ // className). They are all valid div attributes, so the cast is safe.
27
+ return <div {...(rest as HTMLAttributes<HTMLDivElement>)}>{children}</div>
28
+ }
29
+ Main.displayName = 'main'
30
+
31
+ /** Schema for the default {@link Main} root component. Type is exactly `main`. */
32
+ export const mainSchema: SchemaType = createSchema({
33
+ type: 'main',
34
+ title: 'Main',
35
+ })
36
+
37
+ /**
38
+ * Default `main` component module, registered ahead of user components by
39
+ * {@link registerWeaverseNextComponents}. Kept package-internal — consumers
40
+ * override it simply by registering their own component with `type: 'main'`.
41
+ */
42
+ export const defaultMainComponent: WeaverseNextComponent = {
43
+ default: Main,
44
+ schema: mainSchema,
45
+ }
@@ -0,0 +1,223 @@
1
+ 'use client'
2
+
3
+ import {
4
+ createContext,
5
+ type ReactNode,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ } from 'react'
11
+ import { WeaverseNextRootContext } from './root-provider'
12
+ import { createWeaverseNextThemeSettingsStore } from './theme-settings-store'
13
+ import type { TranslateFunction } from './translate'
14
+ import { TranslationProvider } from './translation-context'
15
+ import { TranslationStore } from './translation-store'
16
+ import type {
17
+ WeaverseNextClient,
18
+ WeaverseNextCommerceContext,
19
+ WeaverseNextThemeSettingsStore,
20
+ } from './types'
21
+
22
+ const EMPTY_THEME_SETTINGS: Record<string, unknown> = {}
23
+ const EMPTY_STATIC_CONTENT: Record<string, unknown> = {}
24
+
25
+ /** Read `themeSchema.i18n.staticContent` for the fallback (no-root) provider. */
26
+ export function getSchemaStaticContent(
27
+ schema: unknown
28
+ ): Record<string, unknown> {
29
+ let i18n = (schema as { i18n?: { staticContent?: Record<string, unknown> } })
30
+ ?.i18n
31
+ return i18n?.staticContent ?? EMPTY_STATIC_CONTENT
32
+ }
33
+
34
+ /**
35
+ * Value carried by {@link WeaverseNextContext}. Holds the explicit data
36
+ * boundaries that replace React Router's `useLoaderData` /
37
+ * `useRouteLoaderData` inside sections.
38
+ */
39
+ export interface WeaverseNextContextValue {
40
+ /** Client used by the renderer and component-loader compatibility hooks. */
41
+ client?: WeaverseNextClient
42
+ /** App-provided commerce context for rendered components. */
43
+ commerce?: WeaverseNextCommerceContext
44
+ /** Active-locale static-text overrides, threaded into the runtime. */
45
+ merchantOverrides?: Record<string, unknown>
46
+ /** Page-scoped data exposed to rendered components. */
47
+ pageData?: unknown
48
+ /** Root-scoped data exposed to rendered components. */
49
+ rootData?: unknown
50
+ /** Current merged theme-setting values. */
51
+ themeSettings: Record<string, unknown>
52
+ /** External store that publishes live theme-setting updates. */
53
+ themeSettingsStore: WeaverseNextThemeSettingsStore
54
+ /** Live design-mode static-text override store, threaded into the runtime. */
55
+ translationStore: TranslationStore
56
+ }
57
+
58
+ /**
59
+ * Null sentinel default lets hooks detect usage outside the provider and throw
60
+ * a clear error instead of silently returning empty data.
61
+ */
62
+ export const WeaverseNextContext =
63
+ createContext<WeaverseNextContextValue | null>(null)
64
+
65
+ /** Explicit page, commerce, theme, and translation data for Next components. */
66
+ export interface WeaverseNextProviderProps {
67
+ /** React subtree that consumes the Weaverse Next contexts. */
68
+ children: ReactNode
69
+ /** Client used by the renderer and component-loader compatibility hooks. */
70
+ client?: WeaverseNextClient
71
+ /** App-provided commerce context (storefront/cart/customer). */
72
+ commerce?: WeaverseNextCommerceContext
73
+ /**
74
+ * Active-locale static-text overrides (nested, from `loadThemeSettings()`).
75
+ * Adopts the root provider's `merchantOverrides` when omitted.
76
+ */
77
+ merchantOverrides?: Record<string, unknown>
78
+ /** Explicit page/route commerce data (replaces section-level `useLoaderData`). */
79
+ pageData?: unknown
80
+ /** Explicit root/global data (replaces `useRouteLoaderData('root')`). */
81
+ rootData?: unknown
82
+ /**
83
+ * Theme default locale JSON (`loadThemeSettings().staticContent`). Adopts the
84
+ * root provider's `staticContent` when omitted, then falls back to
85
+ * `client.themeSchema.i18n.staticContent`.
86
+ */
87
+ staticContent?: Record<string, unknown>
88
+ /** Optional external translation function (host i18n); highest priority in `t()`. */
89
+ t?: TranslateFunction
90
+ /** Theme settings override; falls back to `client.themeSettings`. */
91
+ themeSettings?: Record<string, unknown>
92
+ }
93
+
94
+ /**
95
+ * Client boundary that supplies explicit root/page/commerce data to Weaverse
96
+ * sections. Next-native replacement for `withWeaverse` + route loader data —
97
+ * no React Router route matches are emulated.
98
+ *
99
+ * In App Router, create/pass `client` only from a client component wrapper. A
100
+ * `WeaverseNextClient` contains functions and component references, so it is
101
+ * not serializable across a Server Component → Client Component boundary.
102
+ *
103
+ * @example
104
+ * ```tsx
105
+ * 'use client'
106
+ *
107
+ * <WeaverseNextProvider client={client} rootData={rootData} pageData={pageData} commerce={commerce}>
108
+ * <WeaverseNextRenderer />
109
+ * </WeaverseNextProvider>
110
+ * ```
111
+ */
112
+ export function WeaverseNextProvider(props: WeaverseNextProviderProps) {
113
+ let { children, client, rootData, pageData, commerce, themeSettings } = props
114
+ let rootContext = useContext(WeaverseNextRootContext)
115
+
116
+ let themeSettingsValue =
117
+ themeSettings ?? client?.themeSettings ?? EMPTY_THEME_SETTINGS
118
+
119
+ // Fallback translation store when no `WeaverseNextRootProvider` is mounted.
120
+ // Otherwise the ambient root store is adopted, so exactly one live static-text
121
+ // store backs Builder's `updateStaticText()` RPC per page load — mirroring the
122
+ // theme-settings store adoption below. `useRef`, not `useMemo`: React may
123
+ // evict memoized values, but this store must survive every re-render so
124
+ // design-mode overrides already applied by Builder are never dropped.
125
+ let ownTranslationStoreRef = useRef<TranslationStore | null>(null)
126
+ if (!(rootContext || ownTranslationStoreRef.current)) {
127
+ ownTranslationStoreRef.current = new TranslationStore()
128
+ }
129
+ let translationStore =
130
+ rootContext?.translationStore ??
131
+ (ownTranslationStoreRef.current as TranslationStore)
132
+
133
+ // Static content / merchant overrides feeding `useTranslation()`. Precedence:
134
+ // explicit prop → ambient root value → (static content only) schema default.
135
+ let staticContent =
136
+ props.staticContent ??
137
+ rootContext?.staticContent ??
138
+ getSchemaStaticContent(client?.themeSchema)
139
+ let merchantOverrides =
140
+ props.merchantOverrides ?? rootContext?.merchantOverrides
141
+
142
+ // Fallback store for apps that don't mount `WeaverseNextRootProvider`. Not
143
+ // created at all when a root store is present, so exactly one
144
+ // `WeaverseNextThemeSettingsStore` exists per page load either way.
145
+ let ownThemeSettingsStore = useMemo(() => {
146
+ if (rootContext) {
147
+ return null
148
+ }
149
+ return createWeaverseNextThemeSettingsStore({
150
+ schema: client?.themeSchema,
151
+ settings: themeSettingsValue,
152
+ })
153
+ }, [rootContext, client?.themeSchema, themeSettingsValue])
154
+
155
+ let themeSettingsStore =
156
+ rootContext?.themeSettingsStore ??
157
+ (ownThemeSettingsStore as WeaverseNextThemeSettingsStore)
158
+
159
+ // When adopting the root store, route-supplied theme data augments it in
160
+ // place (merge), never replaces it — Studio and root `Header`/`Footer` must
161
+ // keep observing the same instance. Guarded so a stable `themeSettingsValue`
162
+ // across re-renders doesn't re-merge/re-notify every render.
163
+ //
164
+ // This runs in an effect, not render: mutating a store shared outside this
165
+ // component's subtree is a side effect, and render functions must stay
166
+ // pure under concurrent React (retried/discarded renders, StrictMode
167
+ // double-invocation). The root's initial theme is authoritative for SSR;
168
+ // a route's `themeSettings`/`client.themeSettings` override applies after
169
+ // client mount instead of racing a render-phase store mutation. Apps that
170
+ // don't mount `WeaverseNextRootProvider` are unaffected — `themeSettings`
171
+ // still renders synchronously on SSR via `ownThemeSettingsStore` below.
172
+ let mergedIntoRootRef = useRef<Record<string, unknown> | undefined>(undefined)
173
+ useEffect(() => {
174
+ if (
175
+ rootContext &&
176
+ themeSettingsValue !== EMPTY_THEME_SETTINGS &&
177
+ mergedIntoRootRef.current !== themeSettingsValue
178
+ ) {
179
+ rootContext.themeSettingsStore.updateThemeSettings(themeSettingsValue)
180
+ mergedIntoRootRef.current = themeSettingsValue
181
+ }
182
+ }, [rootContext, themeSettingsValue])
183
+
184
+ // Do not depend on `themeSettingsValue` here. In fallback mode it is already
185
+ // represented by a new `themeSettingsStore`; in root-adoption mode the
186
+ // post-mount merge effect notifies `useThemeSettings()` subscribers. Including
187
+ // it would recreate this context with a stale root snapshot before the effect
188
+ // fires, causing an avoidable extra render.
189
+ let value = useMemo<WeaverseNextContextValue>(
190
+ () => ({
191
+ client,
192
+ rootData,
193
+ pageData,
194
+ commerce: commerce ?? client?.commerce,
195
+ merchantOverrides,
196
+ themeSettings: themeSettingsStore.getSnapshot(),
197
+ themeSettingsStore,
198
+ translationStore,
199
+ }),
200
+ [
201
+ client,
202
+ rootData,
203
+ pageData,
204
+ commerce,
205
+ merchantOverrides,
206
+ themeSettingsStore,
207
+ translationStore,
208
+ ]
209
+ )
210
+
211
+ return (
212
+ <WeaverseNextContext.Provider value={value}>
213
+ <TranslationProvider
214
+ merchantOverrides={merchantOverrides}
215
+ staticContent={staticContent}
216
+ t={props.t}
217
+ translationStore={translationStore}
218
+ >
219
+ {children}
220
+ </TranslationProvider>
221
+ </WeaverseNextContext.Provider>
222
+ )
223
+ }
@@ -0,0 +1,63 @@
1
+ import { Weaverse } from '@weaverse/react'
2
+ import { ensureNextItemConstructor } from './item'
3
+ import { defaultMainComponent } from './main'
4
+ import type { WeaverseNextComponent } from './types'
5
+
6
+ const MAIN_TYPE = 'main'
7
+
8
+ /**
9
+ * Component types already pushed into the element registry. Registration is
10
+ * idempotent by `schema.type`, so callers may pass fresh arrays per request
11
+ * without causing repeated global registry writes.
12
+ */
13
+ const registeredComponentTypes = new Set<string>()
14
+
15
+ /**
16
+ * Register Next adapter components with the shared Weaverse element registry.
17
+ *
18
+ * This mutates global Weaverse runtime state, so callers should run it while
19
+ * creating their app/client wiring, not from a React render body.
20
+ *
21
+ * A default `main` root component is always registered first so consumers don't
22
+ * have to register one for basic page-tree rendering. If the consumer registers
23
+ * their own `main`, that one is preferred: we only prepend the default when the
24
+ * provided components don't already include a `main`, so a user-provided `main`
25
+ * never collides with (or is shadowed by) the default.
26
+ */
27
+ export function registerWeaverseNextComponents(
28
+ components: WeaverseNextComponent[]
29
+ ) {
30
+ ensureNextItemConstructor()
31
+
32
+ let hasUserMain = components.some(
33
+ (component) => component.schema?.type === MAIN_TYPE
34
+ )
35
+ let effectiveComponents = hasUserMain
36
+ ? components
37
+ : [defaultMainComponent, ...components]
38
+
39
+ for (let component of effectiveComponents) {
40
+ let type = component.schema.type
41
+ let element = {
42
+ type,
43
+ Component: component.default,
44
+ schema: component.schema,
45
+ loader: component.loader,
46
+ }
47
+
48
+ if (type === MAIN_TYPE && hasUserMain) {
49
+ // `main` is the only built-in default. If a consumer registers their own
50
+ // root component after the default was already installed by another
51
+ // client/test/request, prefer the consumer version by replacing the global
52
+ // registry entry explicitly.
53
+ Weaverse.elementRegistry.set(type, element)
54
+ registeredComponentTypes.add(type)
55
+ continue
56
+ }
57
+
58
+ if (type && !registeredComponentTypes.has(type)) {
59
+ Weaverse.registerElement(element)
60
+ registeredComponentTypes.add(type)
61
+ }
62
+ }
63
+ }