@weaverse/next 0.1.0-alpha.12 → 0.1.0-alpha.14

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 +125 -23
  2. package/dist/{chunk-MQ6N5ODA.mjs → chunk-NL22OVD7.mjs} +93 -4
  3. package/dist/chunk-NL22OVD7.mjs.map +1 -0
  4. package/dist/index.js +98 -4
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +32 -26
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/server.js +158 -2
  9. package/dist/server.js.map +1 -1
  10. package/dist/server.mjs +126 -5
  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 +54 -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 +20 -0
  29. package/dist/types/renderer.d.ts.map +1 -0
  30. package/dist/types/request-info.d.ts +8 -0
  31. package/dist/types/request-info.d.ts.map +1 -0
  32. package/dist/types/revalidate-item.d.ts +81 -0
  33. package/dist/types/revalidate-item.d.ts.map +1 -0
  34. package/dist/types/root-provider.d.ts +61 -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 +17 -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 +661 -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 +7 -6
  71. package/src/client.ts +101 -0
  72. package/src/hooks.ts +50 -0
  73. package/src/index.ts +153 -0
  74. package/src/item.ts +151 -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 +99 -0
  80. package/src/request-info.ts +95 -0
  81. package/src/revalidate-item.ts +210 -0
  82. package/src/root-provider.tsx +181 -0
  83. package/src/runtime.ts +521 -0
  84. package/src/seo.ts +157 -0
  85. package/src/server/configs.ts +182 -0
  86. package/src/server/normalize-page-url.ts +65 -0
  87. package/src/server/revalidate-handler.ts +357 -0
  88. package/src/server/server-client.ts +727 -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 +125 -0
  94. package/src/theme-settings-store.ts +46 -0
  95. package/src/translation-context.tsx +299 -0
  96. package/src/translation-store.ts +69 -0
  97. package/src/types.ts +748 -0
  98. package/src/use-weaverse-next-studio.tsx +106 -0
  99. package/src/utils.ts +37 -0
  100. package/dist/chunk-MQ6N5ODA.mjs.map +0 -1
  101. package/dist/index.d.mts +0 -592
  102. package/dist/index.d.ts +0 -592
  103. package/dist/seo-CrEFl_Up.d.mts +0 -516
  104. package/dist/seo-CrEFl_Up.d.ts +0 -516
  105. package/dist/server.d.mts +0 -100
  106. package/dist/server.d.ts +0 -100
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,225 @@
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 {
14
+ type TranslateFunction,
15
+ TranslationProvider,
16
+ } from './translation-context'
17
+ import { TranslationStore } from './translation-store'
18
+ import type {
19
+ WeaverseNextClient,
20
+ WeaverseNextCommerceContext,
21
+ WeaverseNextThemeSettingsStore,
22
+ } from './types'
23
+
24
+ const EMPTY_THEME_SETTINGS: Record<string, unknown> = {}
25
+ const EMPTY_STATIC_CONTENT: Record<string, unknown> = {}
26
+
27
+ /** Read `themeSchema.i18n.staticContent` for the fallback (no-root) provider. */
28
+ export function getSchemaStaticContent(
29
+ schema: unknown
30
+ ): Record<string, unknown> {
31
+ let i18n = (schema as { i18n?: { staticContent?: Record<string, unknown> } })
32
+ ?.i18n
33
+ return i18n?.staticContent ?? EMPTY_STATIC_CONTENT
34
+ }
35
+
36
+ /**
37
+ * Value carried by {@link WeaverseNextContext}. Holds the explicit data
38
+ * boundaries that replace React Router's `useLoaderData` /
39
+ * `useRouteLoaderData` inside sections.
40
+ */
41
+ export interface WeaverseNextContextValue {
42
+ /** Client used by the renderer and component-loader compatibility hooks. */
43
+ client?: WeaverseNextClient
44
+ /** App-provided commerce context for rendered components. */
45
+ commerce?: WeaverseNextCommerceContext
46
+ /** Active-locale static-text overrides, threaded into the runtime. */
47
+ merchantOverrides?: Record<string, unknown>
48
+ /** Page-scoped data exposed to rendered components. */
49
+ pageData?: unknown
50
+ /** Root-scoped data exposed to rendered components. */
51
+ rootData?: unknown
52
+ /** Current merged theme-setting values. */
53
+ themeSettings: Record<string, unknown>
54
+ /** External store that publishes live theme-setting updates. */
55
+ themeSettingsStore: WeaverseNextThemeSettingsStore
56
+ /** Live design-mode static-text override store, threaded into the runtime. */
57
+ translationStore: TranslationStore
58
+ }
59
+
60
+ /**
61
+ * Null sentinel default lets hooks detect usage outside the provider and throw
62
+ * a clear error instead of silently returning empty data.
63
+ */
64
+ export const WeaverseNextContext =
65
+ createContext<WeaverseNextContextValue | null>(null)
66
+
67
+ /** Explicit page, commerce, theme, and translation data for Next components. */
68
+ export interface WeaverseNextProviderProps {
69
+ /** React subtree that consumes the Weaverse Next contexts. */
70
+ children: ReactNode
71
+ /** Client used by the renderer and component-loader compatibility hooks. */
72
+ client?: WeaverseNextClient
73
+ /** App-provided commerce context (storefront/cart/customer). */
74
+ commerce?: WeaverseNextCommerceContext
75
+ /**
76
+ * Active-locale static-text overrides (nested, from `loadThemeSettings()`).
77
+ * Adopts the root provider's `merchantOverrides` when omitted.
78
+ */
79
+ merchantOverrides?: Record<string, unknown>
80
+ /** Explicit page/route commerce data (replaces section-level `useLoaderData`). */
81
+ pageData?: unknown
82
+ /** Explicit root/global data (replaces `useRouteLoaderData('root')`). */
83
+ rootData?: unknown
84
+ /**
85
+ * Theme default locale JSON (`loadThemeSettings().staticContent`). Adopts the
86
+ * root provider's `staticContent` when omitted, then falls back to
87
+ * `client.themeSchema.i18n.staticContent`.
88
+ */
89
+ staticContent?: Record<string, unknown>
90
+ /** Optional external translation function (host i18n); highest priority in `t()`. */
91
+ t?: TranslateFunction
92
+ /** Theme settings override; falls back to `client.themeSettings`. */
93
+ themeSettings?: Record<string, unknown>
94
+ }
95
+
96
+ /**
97
+ * Client boundary that supplies explicit root/page/commerce data to Weaverse
98
+ * sections. Next-native replacement for `withWeaverse` + route loader data —
99
+ * no React Router route matches are emulated.
100
+ *
101
+ * In App Router, create/pass `client` only from a client component wrapper. A
102
+ * `WeaverseNextClient` contains functions and component references, so it is
103
+ * not serializable across a Server Component → Client Component boundary.
104
+ *
105
+ * @example
106
+ * ```tsx
107
+ * 'use client'
108
+ *
109
+ * <WeaverseNextProvider client={client} rootData={rootData} pageData={pageData} commerce={commerce}>
110
+ * <WeaverseNextRenderer />
111
+ * </WeaverseNextProvider>
112
+ * ```
113
+ */
114
+ export function WeaverseNextProvider(props: WeaverseNextProviderProps) {
115
+ let { children, client, rootData, pageData, commerce, themeSettings } = props
116
+ let rootContext = useContext(WeaverseNextRootContext)
117
+
118
+ let themeSettingsValue =
119
+ themeSettings ?? client?.themeSettings ?? EMPTY_THEME_SETTINGS
120
+
121
+ // Fallback translation store when no `WeaverseNextRootProvider` is mounted.
122
+ // Otherwise the ambient root store is adopted, so exactly one live static-text
123
+ // store backs Builder's `updateStaticText()` RPC per page load — mirroring the
124
+ // theme-settings store adoption below. `useRef`, not `useMemo`: React may
125
+ // evict memoized values, but this store must survive every re-render so
126
+ // design-mode overrides already applied by Builder are never dropped.
127
+ let ownTranslationStoreRef = useRef<TranslationStore | null>(null)
128
+ if (!(rootContext || ownTranslationStoreRef.current)) {
129
+ ownTranslationStoreRef.current = new TranslationStore()
130
+ }
131
+ let translationStore =
132
+ rootContext?.translationStore ??
133
+ (ownTranslationStoreRef.current as TranslationStore)
134
+
135
+ // Static content / merchant overrides feeding `useTranslation()`. Precedence:
136
+ // explicit prop → ambient root value → (static content only) schema default.
137
+ let staticContent =
138
+ props.staticContent ??
139
+ rootContext?.staticContent ??
140
+ getSchemaStaticContent(client?.themeSchema)
141
+ let merchantOverrides =
142
+ props.merchantOverrides ?? rootContext?.merchantOverrides
143
+
144
+ // Fallback store for apps that don't mount `WeaverseNextRootProvider`. Not
145
+ // created at all when a root store is present, so exactly one
146
+ // `WeaverseNextThemeSettingsStore` exists per page load either way.
147
+ let ownThemeSettingsStore = useMemo(() => {
148
+ if (rootContext) {
149
+ return null
150
+ }
151
+ return createWeaverseNextThemeSettingsStore({
152
+ schema: client?.themeSchema,
153
+ settings: themeSettingsValue,
154
+ })
155
+ }, [rootContext, client?.themeSchema, themeSettingsValue])
156
+
157
+ let themeSettingsStore =
158
+ rootContext?.themeSettingsStore ??
159
+ (ownThemeSettingsStore as WeaverseNextThemeSettingsStore)
160
+
161
+ // When adopting the root store, route-supplied theme data augments it in
162
+ // place (merge), never replaces it — Studio and root `Header`/`Footer` must
163
+ // keep observing the same instance. Guarded so a stable `themeSettingsValue`
164
+ // across re-renders doesn't re-merge/re-notify every render.
165
+ //
166
+ // This runs in an effect, not render: mutating a store shared outside this
167
+ // component's subtree is a side effect, and render functions must stay
168
+ // pure under concurrent React (retried/discarded renders, StrictMode
169
+ // double-invocation). The root's initial theme is authoritative for SSR;
170
+ // a route's `themeSettings`/`client.themeSettings` override applies after
171
+ // client mount instead of racing a render-phase store mutation. Apps that
172
+ // don't mount `WeaverseNextRootProvider` are unaffected — `themeSettings`
173
+ // still renders synchronously on SSR via `ownThemeSettingsStore` below.
174
+ let mergedIntoRootRef = useRef<Record<string, unknown> | undefined>(undefined)
175
+ useEffect(() => {
176
+ if (
177
+ rootContext &&
178
+ themeSettingsValue !== EMPTY_THEME_SETTINGS &&
179
+ mergedIntoRootRef.current !== themeSettingsValue
180
+ ) {
181
+ rootContext.themeSettingsStore.updateThemeSettings(themeSettingsValue)
182
+ mergedIntoRootRef.current = themeSettingsValue
183
+ }
184
+ }, [rootContext, themeSettingsValue])
185
+
186
+ // Do not depend on `themeSettingsValue` here. In fallback mode it is already
187
+ // represented by a new `themeSettingsStore`; in root-adoption mode the
188
+ // post-mount merge effect notifies `useThemeSettings()` subscribers. Including
189
+ // it would recreate this context with a stale root snapshot before the effect
190
+ // fires, causing an avoidable extra render.
191
+ let value = useMemo<WeaverseNextContextValue>(
192
+ () => ({
193
+ client,
194
+ rootData,
195
+ pageData,
196
+ commerce: commerce ?? client?.commerce,
197
+ merchantOverrides,
198
+ themeSettings: themeSettingsStore.getSnapshot(),
199
+ themeSettingsStore,
200
+ translationStore,
201
+ }),
202
+ [
203
+ client,
204
+ rootData,
205
+ pageData,
206
+ commerce,
207
+ merchantOverrides,
208
+ themeSettingsStore,
209
+ translationStore,
210
+ ]
211
+ )
212
+
213
+ return (
214
+ <WeaverseNextContext.Provider value={value}>
215
+ <TranslationProvider
216
+ merchantOverrides={merchantOverrides}
217
+ staticContent={staticContent}
218
+ t={props.t}
219
+ translationStore={translationStore}
220
+ >
221
+ {children}
222
+ </TranslationProvider>
223
+ </WeaverseNextContext.Provider>
224
+ )
225
+ }
@@ -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
+ }
@@ -0,0 +1,99 @@
1
+ 'use client'
2
+
3
+ import { WeaverseRoot } from '@weaverse/react'
4
+ import { memo, useContext, useEffect, useLayoutEffect, useMemo } from 'react'
5
+ import { WeaverseNextContext } from './provider'
6
+ import { createWeaverseNextRuntime } from './runtime'
7
+ import type {
8
+ WeaverseNextClient,
9
+ WeaverseNextLoaderData,
10
+ WeaverseNextPageData,
11
+ } from './types'
12
+ import { WeaverseNextStudio } from './use-weaverse-next-studio'
13
+
14
+ const EMPTY_DATA_CONTEXT: Record<string, unknown> = {}
15
+ const useIsomorphicLayoutEffect =
16
+ typeof window === 'undefined' ? useEffect : useLayoutEffect
17
+
18
+ function getRenderablePage(
19
+ data: WeaverseNextLoaderData | null | undefined
20
+ ): (WeaverseNextPageData & { rootId: string }) | null {
21
+ let page = data?.page
22
+ if (!page) {
23
+ return null
24
+ }
25
+ let rootId =
26
+ page.rootId ??
27
+ page.items.find((item) => item.type === 'main')?.id ??
28
+ page.items[0]?.id ??
29
+ page.id
30
+ return { ...page, rootId }
31
+ }
32
+
33
+ /** Page payload, client, and connector data accepted by the Next renderer. */
34
+ export interface WeaverseNextRendererProps {
35
+ /** Client to render from. Defaults to the provider's client. */
36
+ client?: WeaverseNextClient
37
+ /** Page payload to render. Defaults to `client.data`. */
38
+ data?: WeaverseNextLoaderData | null
39
+ /** Data-connector context for `{{...}}` template replacement. */
40
+ dataContext?: Record<string, unknown>
41
+ }
42
+
43
+ /**
44
+ * Render a serialized Weaverse page tree through `@weaverse/react` primitives.
45
+ *
46
+ * Reads the client/page data supplied by {@link WeaverseNextProvider} when no
47
+ * props are passed, or accepts an explicit `client` + `data` for standalone use. Builds a
48
+ * framework-neutral `Weaverse` core instance (no React Router hooks) and hands
49
+ * it to the shared `WeaverseRoot`.
50
+ */
51
+ export const WeaverseNextRenderer = memo(function WeaverseNextRendererComponent(
52
+ props: WeaverseNextRendererProps
53
+ ) {
54
+ let context = useContext(WeaverseNextContext)
55
+ let client = props.client ?? context?.client
56
+ let data = props.data ?? client?.data ?? null
57
+ let dataContext =
58
+ props.dataContext ?? client?.dataContext ?? EMPTY_DATA_CONTEXT
59
+
60
+ let page = useMemo(() => getRenderablePage(data), [data])
61
+
62
+ let weaverse = useMemo(() => {
63
+ if (!page) {
64
+ return null
65
+ }
66
+
67
+ return createWeaverseNextRuntime({
68
+ client,
69
+ data: { ...(data as WeaverseNextLoaderData), page },
70
+ dataContext,
71
+ merchantOverrides: context?.merchantOverrides,
72
+ themeSettingsStore: context?.themeSettingsStore,
73
+ translationStore: context?.translationStore,
74
+ })
75
+ }, [
76
+ client,
77
+ page,
78
+ data,
79
+ dataContext,
80
+ context?.merchantOverrides,
81
+ context?.themeSettingsStore,
82
+ context?.translationStore,
83
+ ])
84
+
85
+ useIsomorphicLayoutEffect(() => {
86
+ weaverse?.flushRenderPhaseUpdates()
87
+ }, [weaverse])
88
+
89
+ if (!weaverse) {
90
+ return null
91
+ }
92
+
93
+ return (
94
+ <>
95
+ <WeaverseRoot context={weaverse} />
96
+ <WeaverseNextStudio runtime={weaverse} />
97
+ </>
98
+ )
99
+ })
@@ -0,0 +1,95 @@
1
+ import type {
2
+ WeaverseNextRequestContext,
3
+ WeaverseNextRequestInfo,
4
+ } from './types'
5
+
6
+ function toUrl(value: string | URL): URL {
7
+ return typeof value === 'string' ? new URL(value, 'http://localhost') : value
8
+ }
9
+
10
+ function getSearchParamsFromContext(
11
+ context?: WeaverseNextRequestContext
12
+ ): URLSearchParams {
13
+ if (context?.searchParams) {
14
+ return new URLSearchParams(context.searchParams)
15
+ }
16
+
17
+ if (context?.url) {
18
+ let url = toUrl(context.url)
19
+ return new URLSearchParams(url.searchParams)
20
+ }
21
+
22
+ return new URLSearchParams()
23
+ }
24
+
25
+ function getPathnameFromContext(context?: WeaverseNextRequestContext): string {
26
+ if (context?.pathname) {
27
+ return context.pathname
28
+ }
29
+
30
+ if (context?.url) {
31
+ let url = toUrl(context.url)
32
+ return url.pathname || '/'
33
+ }
34
+
35
+ return '/'
36
+ }
37
+
38
+ function coerceQueryValue(value: string): string | boolean {
39
+ if (value === 'true') {
40
+ return true
41
+ }
42
+ if (value === 'false') {
43
+ return false
44
+ }
45
+ return value
46
+ }
47
+
48
+ function toQueryRecord(
49
+ searchParams: URLSearchParams
50
+ ): Record<string, string | boolean> {
51
+ let queries: Record<string, string | boolean> = {}
52
+ for (let [key, value] of searchParams.entries()) {
53
+ queries[key] = coerceQueryValue(value)
54
+ }
55
+ return queries
56
+ }
57
+
58
+ const TRANSIENT_STUDIO_PARAMS = new Set([
59
+ 'weaverseDraftItem',
60
+ // Legacy name from the original design-mode draft-item spike. Keep it out of
61
+ // runtime requestInfo too, in case an older Builder bundle is in front of a
62
+ // newer Next storefront.
63
+ '__weaverseDraftItem',
64
+ ])
65
+
66
+ function getRequestInfoSearchParams(
67
+ context?: WeaverseNextRequestContext
68
+ ): URLSearchParams {
69
+ let searchParams = new URLSearchParams(getSearchParamsFromContext(context))
70
+ for (let key of TRANSIENT_STUDIO_PARAMS) {
71
+ searchParams.delete(key)
72
+ }
73
+ return searchParams
74
+ }
75
+
76
+ /**
77
+ * Build the Hydrogen-like request info shape consumed by the Studio bridge.
78
+ * Duplicate query keys keep the last value, matching `URLSearchParams` iteration
79
+ * assignment and Hydrogen's practical query-object behavior.
80
+ */
81
+ export function buildWeaverseNextRequestInfo(
82
+ context?: WeaverseNextRequestContext
83
+ ): WeaverseNextRequestInfo {
84
+ let searchParams = getRequestInfoSearchParams(context)
85
+ let search = searchParams.toString()
86
+
87
+ return {
88
+ pathname: getPathnameFromContext(context),
89
+ search: search ? `?${search}` : '',
90
+ queries: toQueryRecord(searchParams),
91
+ ...(context?.i18n ? { i18n: context.i18n } : {}),
92
+ ...(context?.pageType ? { pageType: context.pageType } : {}),
93
+ ...(context?.handle ? { handle: context.handle } : {}),
94
+ }
95
+ }