@weaverse/react 5.17.0 → 5.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/hooks.ts ADDED
@@ -0,0 +1,69 @@
1
+ import { Weaverse, type WeaverseItemStore } from '@weaverse/core'
2
+ import { useContext } from 'react'
3
+ import { WeaverseContext, WeaverseItemContext } from './context'
4
+
5
+ export type { DataContext } from './utils/data-connector'
6
+ // Re-export the data connector utilities
7
+ export {
8
+ replaceContentDataConnectors,
9
+ replaceContentDataConnectorsDeep,
10
+ } from './utils/data-connector'
11
+
12
+ /**
13
+ * Returns the active Weaverse runtime from React context.
14
+ *
15
+ * @typeParam T - Runtime subtype expected by the integration.
16
+ */
17
+ export function useWeaverse<T = Weaverse>() {
18
+ let weaverse = useContext(WeaverseContext)
19
+ return weaverse as T
20
+ }
21
+
22
+ /**
23
+ * Returns a Weaverse item instance by ID, or the current rendered item when no
24
+ * ID is provided.
25
+ *
26
+ * @param id - Optional item ID to look up.
27
+ * @returns The matching item instance, or `null` when it is unavailable.
28
+ */
29
+ export let useItemInstance = (id?: string) => {
30
+ let { id: currentId } = useContext(WeaverseItemContext)
31
+ let instance = Weaverse.itemInstances.get(id || currentId)
32
+ if (!instance) {
33
+ console.log(`Item instance ${id} not found`)
34
+ return null
35
+ }
36
+ return instance as WeaverseItemStore
37
+ }
38
+
39
+ /**
40
+ * Returns the parent instance of the current rendered item.
41
+ *
42
+ * @returns The parent item instance. At the root, the empty parent ID falls
43
+ * back to the current item instance; returns `null` only when that lookup is
44
+ * unavailable.
45
+ */
46
+ export let useParentInstance = () => {
47
+ let { parentId } = useContext(WeaverseItemContext)
48
+ return useItemInstance(parentId || '')
49
+ }
50
+
51
+ /**
52
+ * Returns the direct child instances of an item.
53
+ *
54
+ * @param id - Optional parent item ID; defaults to the current rendered item.
55
+ */
56
+ export let useChildInstances = (id?: string) => {
57
+ let currentInstance = useItemInstance(id)
58
+ if (!currentInstance) {
59
+ return []
60
+ }
61
+ let { itemInstances } = Weaverse
62
+
63
+ let {
64
+ data: { children },
65
+ } = currentInstance
66
+ return children
67
+ .map(({ id }: { id: string }) => itemInstances.get(id))
68
+ .filter(Boolean) as WeaverseItemStore[]
69
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * React bindings, rendering primitives, and hooks for Weaverse applications.
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+ export * from '@weaverse/core'
7
+ export * from './context'
8
+ export * from './hooks'
9
+ export * from './renderer'
10
+ export * from './types'
@@ -0,0 +1,204 @@
1
+ import type { Weaverse } from '@weaverse/core'
2
+ import clsx from 'clsx'
3
+ import React, { memo, useEffect, useMemo, useRef } from 'react'
4
+ import { WeaverseContextProvider, WeaverseItemContext } from './context'
5
+ import { useItemInstance, useWeaverse } from './hooks'
6
+ import type { ItemComponentProps, WeaverseRootPropsType } from './types'
7
+ import { replaceContentDataConnectorsDeep } from './utils/data-connector'
8
+
9
+ const REACT_VERSION_THRESHOLD = 18
10
+ const reactVersion = Number(React.version?.split('.')[0]) || 0
11
+
12
+ /**
13
+ * Reads an external store in React while remaining safe during server rendering
14
+ * and on React versions without `useSyncExternalStore`.
15
+ *
16
+ * @param subscribe - Registers a callback for store changes.
17
+ * @param getSnapshot - Returns the current client snapshot.
18
+ * @param getServerSnapshot - Returns the server snapshot; defaults to `getSnapshot`.
19
+ * @returns The current external-store snapshot.
20
+ */
21
+ export const useSafeExternalStore = (
22
+ subscribe: any,
23
+ getSnapshot: any,
24
+ getServerSnapshot: any = getSnapshot
25
+ ) => {
26
+ // In server environment or during SSR, just return the snapshot directly
27
+ if (typeof window === 'undefined') {
28
+ return getServerSnapshot()
29
+ }
30
+
31
+ // In client environment, use React.useSyncExternalStore if available (React 18+)
32
+ if (React.useSyncExternalStore) {
33
+ // biome-ignore lint/correctness/useHookAtTopLevel: <$>
34
+ return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
35
+ }
36
+
37
+ // Fallback for older React versions - just return the current snapshot
38
+ return getSnapshot()
39
+ }
40
+
41
+ /**
42
+ * Determines the root element ID for the Weaverse component tree.
43
+ * Falls back through multiple strategies to ensure a valid root is always found:
44
+ * 1. Use explicit rootId from context.data if available
45
+ * 2. Find the first element with type 'main' in items array
46
+ * 3. Use projectId as final fallback
47
+ */
48
+ const getRootId = (context: Weaverse): string => {
49
+ // Strategy 1: Explicit rootId
50
+ if (context.data.rootId) {
51
+ return context.data.rootId
52
+ }
53
+
54
+ // Strategy 2: Find main element
55
+ let mainItem = context.data?.items?.find((item) => item.type === 'main')
56
+ if (mainItem?.id) {
57
+ return mainItem.id
58
+ }
59
+
60
+ // Strategy 3: Fallback to projectId
61
+ return context.projectId
62
+ }
63
+ /**
64
+ * Renders a Weaverse content tree and provides its runtime to descendant
65
+ * components.
66
+ */
67
+ export const WeaverseRoot = memo(({ context }: WeaverseRootPropsType) => {
68
+ const data = useSafeExternalStore(
69
+ context.subscribe,
70
+ context.getSnapShot,
71
+ context.getSnapShot
72
+ )
73
+ const rootRef = useRef<HTMLElement>(null)
74
+
75
+ useEffect(() => {
76
+ context.contentRootElement = rootRef.current
77
+ }, [context])
78
+
79
+ const eventHandlers = context?.studioBridge?.eventHandlers || {}
80
+
81
+ if (context.projectId) {
82
+ return (
83
+ <div
84
+ className="weaverse-content-root"
85
+ {...eventHandlers}
86
+ data-weaverse-project-id={context.projectId}
87
+ data-weaverse-template-id={data.id}
88
+ ref={rootRef}
89
+ >
90
+ <WeaverseContextProvider value={context}>
91
+ <ItemInstance id={getRootId(context)} parentId={''} />
92
+ </WeaverseContextProvider>
93
+ </div>
94
+ )
95
+ }
96
+ return null
97
+ })
98
+ WeaverseRoot.displayName = 'WeaverseRoot'
99
+ const ItemComponent = memo(({ instance }: ItemComponentProps) => {
100
+ const context = useWeaverse()
101
+ const { elementRegistry } = context
102
+ const data = useSafeExternalStore(
103
+ instance.subscribe,
104
+ instance.getSnapShot,
105
+ instance.getSnapShot
106
+ )
107
+ const {
108
+ id,
109
+ type,
110
+ childIds = [],
111
+ children = [],
112
+
113
+ createdAt,
114
+ updatedAt,
115
+ deletedAt,
116
+ css,
117
+ __hidden,
118
+ // Need to get rid of this `data` property
119
+ data: _,
120
+ ...rest
121
+ } = data
122
+
123
+ // Replace data connectors in all component data (handles strings, objects, and arrays recursively)
124
+ // IMMUTABLE: Process the entire rest object to avoid mutating original content
125
+ // Memoized per snapshot: `data` identity changes exactly when the item's
126
+ // store or translation entry changes (see WeaverseHydrogenItem.getSnapShot),
127
+ // and context-only updates swap the store reference via `setData({})` in
128
+ // syncReusedInstance — so re-renders with an unchanged snapshot skip the
129
+ // deep placeholder scan + clone entirely.
130
+ // biome-ignore lint/correctness/useExhaustiveDependencies: `rest` is a pure projection of `data`
131
+ const processedRest = useMemo(
132
+ () => replaceContentDataConnectorsDeep(rest, context.dataContext),
133
+ [data, context.dataContext]
134
+ )
135
+
136
+ useEffect(() => {
137
+ if (!instance.ref.current) {
138
+ // Fallback `ref` if the element isn't created by `React.forwardRef`
139
+ Object.assign(instance.ref, {
140
+ current: document.querySelector(`[data-wv-id="${id}"]`),
141
+ })
142
+ }
143
+ }, [instance, id])
144
+
145
+ if (__hidden) {
146
+ return null
147
+ }
148
+
149
+ const Element = elementRegistry.get(type)
150
+
151
+ if (Element?.Component) {
152
+ const Component = Element.Component
153
+ if (Component.displayName !== type) {
154
+ // Devtools nicety — assign once, not on every render.
155
+ Component.displayName = type
156
+ }
157
+ if (
158
+ Component.$$typeof === Symbol.for('react.forward_ref') ||
159
+ reactVersion > REACT_VERSION_THRESHOLD
160
+ ) {
161
+ processedRest.ref = instance.ref
162
+ }
163
+ const childItems = children?.length
164
+ ? children
165
+ : childIds?.length
166
+ ? childIds.map((cid: string) => ({ id: cid }))
167
+ : []
168
+
169
+ const renderChildren = childItems.map(
170
+ (item: { id: string }, index: number) => (
171
+ <ItemInstance id={item.id} key={`${item.id}-${index}`} parentId={id} />
172
+ )
173
+ )
174
+
175
+ return (
176
+ <Component
177
+ {...processedRest}
178
+ children={renderChildren.length ? renderChildren : undefined}
179
+ className={clsx(processedRest.data?.className)}
180
+ data-wv-id={id}
181
+ data-wv-type={type}
182
+ key={id}
183
+ />
184
+ )
185
+ }
186
+ instance._store.deleted = true
187
+ return null
188
+ })
189
+ ItemComponent.displayName = 'WeaverseComponent'
190
+
191
+ const ItemInstance = memo(
192
+ ({ id, parentId }: { id: string; parentId: string }) => {
193
+ const instance = useItemInstance(id)
194
+ if (!instance) {
195
+ return <div style={{ display: 'none' }}>Item instance {id} not found</div>
196
+ }
197
+ return (
198
+ <WeaverseItemContext.Provider value={{ parentId, id }}>
199
+ <ItemComponent instance={instance} key={id} />
200
+ </WeaverseItemContext.Provider>
201
+ )
202
+ }
203
+ )
204
+ ItemInstance.displayName = 'WeaverseInstanceProvider'
@@ -0,0 +1,26 @@
1
+ import type { ElementCSS, Weaverse, WeaverseItemStore } from '@weaverse/core'
2
+ import type { ReactElement, ReactNode } from 'react'
3
+
4
+ /** Common props accepted by React components rendered by Weaverse. */
5
+ export interface WeaverseElementProps extends Partial<ReactElement> {
6
+ /** React content rendered inside the element. */
7
+ children?: ReactElement | ReactElement[] | ReactNode | ReactNode[]
8
+ /** CSS class names applied to the element. */
9
+ className?: string
10
+ /** Responsive CSS values configured for the element. */
11
+ css?: ElementCSS
12
+ /** Weaverse item ID associated with the rendered element. */
13
+ 'data-wv-id'?: string
14
+ }
15
+
16
+ /** Props for the root Weaverse React renderer. */
17
+ export type WeaverseRootPropsType = {
18
+ /** Weaverse runtime that owns the rendered content tree. */
19
+ context: Weaverse
20
+ }
21
+
22
+ /** Props used to render a registered component from its item store. */
23
+ export type ItemComponentProps = {
24
+ /** Store instance containing the item's data and subscriptions. */
25
+ instance: InstanceType<typeof WeaverseItemStore>
26
+ }