@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
@@ -0,0 +1,100 @@
1
+ 'use client'
2
+
3
+ import { WeaverseRoot } from '@weaverse/react'
4
+ import { memo, useContext, useEffect, useLayoutEffect, useMemo } from 'react'
5
+ import type { JSX } from 'react/jsx-runtime'
6
+ import { WeaverseNextContext } from './provider'
7
+ import { createWeaverseNextRuntime } from './runtime'
8
+ import type {
9
+ WeaverseNextClient,
10
+ WeaverseNextLoaderData,
11
+ WeaverseNextPageData,
12
+ } from './types'
13
+ import { WeaverseNextStudio } from './use-weaverse-next-studio'
14
+
15
+ const EMPTY_DATA_CONTEXT: Record<string, unknown> = {}
16
+ const useIsomorphicLayoutEffect =
17
+ typeof window === 'undefined' ? useEffect : useLayoutEffect
18
+
19
+ function getRenderablePage(
20
+ data: WeaverseNextLoaderData | null | undefined
21
+ ): (WeaverseNextPageData & { rootId: string }) | null {
22
+ let page = data?.page
23
+ if (!page) {
24
+ return null
25
+ }
26
+ let rootId =
27
+ page.rootId ??
28
+ page.items.find((item) => item.type === 'main')?.id ??
29
+ page.items[0]?.id ??
30
+ page.id
31
+ return { ...page, rootId }
32
+ }
33
+
34
+ /** Page payload, client, and connector data accepted by the Next renderer. */
35
+ export interface WeaverseNextRendererProps {
36
+ /** Client to render from. Defaults to the provider's client. */
37
+ client?: WeaverseNextClient
38
+ /** Page payload to render. Defaults to `client.data`. */
39
+ data?: WeaverseNextLoaderData | null
40
+ /** Data-connector context for `{{...}}` template replacement. */
41
+ dataContext?: Record<string, unknown>
42
+ }
43
+
44
+ /**
45
+ * Render a serialized Weaverse page tree through `@weaverse/react` primitives.
46
+ *
47
+ * Reads the client/page data supplied by {@link WeaverseNextProvider} when no
48
+ * props are passed, or accepts an explicit `client` + `data` for standalone use. Builds a
49
+ * framework-neutral `Weaverse` core instance (no React Router hooks) and hands
50
+ * it to the shared `WeaverseRoot`.
51
+ */
52
+ export const WeaverseNextRenderer = memo(function WeaverseNextRendererComponent(
53
+ props: WeaverseNextRendererProps
54
+ ): JSX.Element | null {
55
+ let context = useContext(WeaverseNextContext)
56
+ let client = props.client ?? context?.client
57
+ let data = props.data ?? client?.data ?? null
58
+ let dataContext =
59
+ props.dataContext ?? client?.dataContext ?? EMPTY_DATA_CONTEXT
60
+
61
+ let page = useMemo(() => getRenderablePage(data), [data])
62
+
63
+ let weaverse = useMemo(() => {
64
+ if (!page) {
65
+ return null
66
+ }
67
+
68
+ return createWeaverseNextRuntime({
69
+ client,
70
+ data: { ...(data as WeaverseNextLoaderData), page },
71
+ dataContext,
72
+ merchantOverrides: context?.merchantOverrides,
73
+ themeSettingsStore: context?.themeSettingsStore,
74
+ translationStore: context?.translationStore,
75
+ })
76
+ }, [
77
+ client,
78
+ page,
79
+ data,
80
+ dataContext,
81
+ context?.merchantOverrides,
82
+ context?.themeSettingsStore,
83
+ context?.translationStore,
84
+ ])
85
+
86
+ useIsomorphicLayoutEffect(() => {
87
+ weaverse?.flushRenderPhaseUpdates()
88
+ }, [weaverse])
89
+
90
+ if (!weaverse) {
91
+ return null
92
+ }
93
+
94
+ return (
95
+ <>
96
+ <WeaverseRoot context={weaverse} />
97
+ <WeaverseNextStudio runtime={weaverse} />
98
+ </>
99
+ )
100
+ })
@@ -0,0 +1,124 @@
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
+ /**
26
+ * Put the request's market prefix (`i18n.pathPrefix`, e.g. `/de-de`) on a
27
+ * pathname that does not carry it yet.
28
+ *
29
+ * A Next app builds its request context from the route path, and with
30
+ * locale-segmented routes that path is usually unprefixed. Studio's address
31
+ * bar follows `requestInfo.pathname`, so without the prefix it snaps back to
32
+ * the default market after every navigation. A path that already carries the
33
+ * prefix, and the default market's empty prefix, pass through unchanged.
34
+ */
35
+ export function withPathPrefix(
36
+ pathname: string,
37
+ i18n?: WeaverseNextRequestContext['i18n']
38
+ ): string {
39
+ let raw = typeof i18n?.pathPrefix === 'string' ? i18n.pathPrefix : ''
40
+ // Split rather than regex-trim: `pathPrefix` is caller input, and an anchored
41
+ // `/\/+$/` backtracks polynomially on long runs of slashes.
42
+ let segments = raw.split('/').filter(Boolean)
43
+ let prefix = segments.length > 0 ? `/${segments.join('/')}` : ''
44
+ if (
45
+ prefix === '' ||
46
+ pathname === prefix ||
47
+ pathname.startsWith(`${prefix}/`)
48
+ ) {
49
+ return pathname
50
+ }
51
+ return pathname === '/' ? prefix : `${prefix}${pathname}`
52
+ }
53
+
54
+ function getPathnameFromContext(context?: WeaverseNextRequestContext): string {
55
+ if (context?.pathname) {
56
+ return withPathPrefix(context.pathname, context.i18n)
57
+ }
58
+
59
+ if (context?.url) {
60
+ let url = toUrl(context.url)
61
+ return withPathPrefix(url.pathname || '/', context.i18n)
62
+ }
63
+
64
+ return '/'
65
+ }
66
+
67
+ function coerceQueryValue(value: string): string | boolean {
68
+ if (value === 'true') {
69
+ return true
70
+ }
71
+ if (value === 'false') {
72
+ return false
73
+ }
74
+ return value
75
+ }
76
+
77
+ function toQueryRecord(
78
+ searchParams: URLSearchParams
79
+ ): Record<string, string | boolean> {
80
+ let queries: Record<string, string | boolean> = {}
81
+ for (let [key, value] of searchParams.entries()) {
82
+ queries[key] = coerceQueryValue(value)
83
+ }
84
+ return queries
85
+ }
86
+
87
+ const TRANSIENT_STUDIO_PARAMS = new Set([
88
+ 'weaverseDraftItem',
89
+ // Legacy name from the original design-mode draft-item spike. Keep it out of
90
+ // runtime requestInfo too, in case an older Builder bundle is in front of a
91
+ // newer Next storefront.
92
+ '__weaverseDraftItem',
93
+ ])
94
+
95
+ function getRequestInfoSearchParams(
96
+ context?: WeaverseNextRequestContext
97
+ ): URLSearchParams {
98
+ let searchParams = new URLSearchParams(getSearchParamsFromContext(context))
99
+ for (let key of TRANSIENT_STUDIO_PARAMS) {
100
+ searchParams.delete(key)
101
+ }
102
+ return searchParams
103
+ }
104
+
105
+ /**
106
+ * Build the Hydrogen-like request info shape consumed by the Studio bridge.
107
+ * Duplicate query keys keep the last value, matching `URLSearchParams` iteration
108
+ * assignment and Hydrogen's practical query-object behavior.
109
+ */
110
+ export function buildWeaverseNextRequestInfo(
111
+ context?: WeaverseNextRequestContext
112
+ ): WeaverseNextRequestInfo {
113
+ let searchParams = getRequestInfoSearchParams(context)
114
+ let search = searchParams.toString()
115
+
116
+ return {
117
+ pathname: getPathnameFromContext(context),
118
+ search: search ? `?${search}` : '',
119
+ queries: toQueryRecord(searchParams),
120
+ ...(context?.i18n ? { i18n: context.i18n } : {}),
121
+ ...(context?.pageType ? { pageType: context.pageType } : {}),
122
+ ...(context?.handle ? { handle: context.handle } : {}),
123
+ }
124
+ }
@@ -0,0 +1,211 @@
1
+ import type { PageType } from '@weaverse/schema'
2
+ import { PageTypeSchema } from '@weaverse/schema'
3
+ import type {
4
+ WeaverseNextComponentData,
5
+ WeaverseNextI18n,
6
+ WeaverseNextRequestInfo,
7
+ } from './types'
8
+
9
+ /** Default App Router endpoint used for Studio item-loader revalidation. */
10
+ export const DEFAULT_REVALIDATE_ENDPOINT = '/api/weaverse/revalidate'
11
+
12
+ const MAX_ROUTE_CONTEXT_HANDLE_LENGTH = 512
13
+
14
+ /**
15
+ * Server-owned and transient controls stripped from the revalidation route
16
+ * context on both sides of the trust boundary. Compared case-insensitively, so
17
+ * every entry is stored lowercased. Client sanitization keeps normal traffic
18
+ * clean; the route handler repeats this deny-list as the security boundary.
19
+ *
20
+ * - Server-owned: a crafted `?weaverseProjectId=`/`?weaverseHost=` must never
21
+ * influence server config resolution (see `getWeaverseNextConfigs`), and
22
+ * credential-shaped params never ride along in route context.
23
+ * - Transient: draft-item and RSC transport params never describe route identity.
24
+ */
25
+ export const DENIED_ROUTE_CONTEXT_PARAMS = [
26
+ 'weaverseprojectid',
27
+ 'weaversehost',
28
+ 'weaverseapikey',
29
+ 'weaverseapibase',
30
+ 'weaversepublicapibase',
31
+ 'weaverseversion',
32
+ 'projectid',
33
+ 'weaversedraftitem',
34
+ '__weaversedraftitem',
35
+ '_rsc',
36
+ ]
37
+
38
+ /** i18n fields allowed to cross the browser boundary. */
39
+ export const ROUTE_CONTEXT_I18N_KEYS = [
40
+ 'country',
41
+ 'label',
42
+ 'language',
43
+ 'locale',
44
+ 'pathPrefix',
45
+ ] as const
46
+
47
+ /**
48
+ * JSON-safe route subset that may cross the browser boundary alongside the
49
+ * draft item. Never carries headers, cookies, auth, env, project ID, Studio
50
+ * host, API keys/bases, commerce clients, the request context, runtime, or
51
+ * client. `search` is canonical (preserves duplicate keys); the derived
52
+ * `queries` record is intentionally omitted.
53
+ */
54
+ export interface WeaverseNextRevalidateRouteContext {
55
+ /** Route handle used for page assignment. */
56
+ handle?: string
57
+ /** Public locale and market identity allowed across the browser boundary. */
58
+ i18n?: Pick<
59
+ WeaverseNextI18n,
60
+ 'country' | 'label' | 'language' | 'locale' | 'pathPrefix'
61
+ >
62
+ /** Weaverse page type resolved by the active route. */
63
+ pageType?: PageType
64
+ /** Canonical same-origin route pathname. */
65
+ pathname: string
66
+ /** Sanitized serialized query string, including the leading `?` when set. */
67
+ search: string
68
+ }
69
+
70
+ /**
71
+ * Minimal runtime surface the in-place apply needs. Structural (not the
72
+ * concrete `WeaverseNextRuntime` class) so unit tests can drive it with a
73
+ * plain mock. `requestInfo` stays optional: a real `WeaverseNextRuntime`
74
+ * always supplies it, but when absent the client omits `routeContext` and the
75
+ * legacy `{ draftItem }` request is preserved.
76
+ */
77
+ export interface RevalidateItemRuntimeLike {
78
+ /** Live item stores keyed by Weaverse item ID. */
79
+ itemInstances: Map<
80
+ string,
81
+ {
82
+ /** Merge refreshed loader data into the live item store. */
83
+ setData: (update: Record<string, unknown>) => unknown
84
+ }
85
+ >
86
+ /** Active route identity used to construct the optional revalidation payload. */
87
+ requestInfo?: WeaverseNextRequestInfo
88
+ }
89
+
90
+ /**
91
+ * Strip denied (server-owned/transient) controls from a `search` string and
92
+ * re-serialize it. Preserves ordinary application queries, duplicate keys,
93
+ * encoded Unicode, and mode controls (`isDesignMode`, `isPreviewMode`,
94
+ * `__revisionId`, `sectionType`). Returns `''` or a `?`-prefixed string.
95
+ */
96
+ export function sanitizeRouteContextSearch(search: string): string {
97
+ let params = new URLSearchParams(search)
98
+ let denied = new Set(DENIED_ROUTE_CONTEXT_PARAMS)
99
+ for (let key of [...new Set(params.keys())]) {
100
+ if (denied.has(key.toLowerCase())) {
101
+ params.delete(key)
102
+ }
103
+ }
104
+ let serialized = params.toString()
105
+ return serialized ? `?${serialized}` : ''
106
+ }
107
+
108
+ /**
109
+ * Build the sanitized {@link WeaverseNextRevalidateRouteContext} from the
110
+ * active runtime's request info. Returns `undefined` without a usable pathname
111
+ * so the caller omits `routeContext` and keeps the legacy request. This is not
112
+ * the security boundary — the route handler re-validates and re-sanitizes.
113
+ */
114
+ export function buildWeaverseNextRevalidateRouteContext(
115
+ requestInfo?: WeaverseNextRequestInfo
116
+ ): WeaverseNextRevalidateRouteContext | undefined {
117
+ if (
118
+ !requestInfo ||
119
+ typeof requestInfo.pathname !== 'string' ||
120
+ !requestInfo.pathname
121
+ ) {
122
+ return
123
+ }
124
+
125
+ let routeContext: WeaverseNextRevalidateRouteContext = {
126
+ pathname: requestInfo.pathname,
127
+ search: sanitizeRouteContextSearch(requestInfo.search ?? ''),
128
+ }
129
+
130
+ let { i18n } = requestInfo
131
+ if (i18n && typeof i18n === 'object') {
132
+ let picked: Record<string, string> = {}
133
+ for (let key of ROUTE_CONTEXT_I18N_KEYS) {
134
+ let value = i18n[key]
135
+ if (typeof value === 'string') {
136
+ picked[key] = value
137
+ }
138
+ }
139
+ if (Object.keys(picked).length) {
140
+ routeContext.i18n = picked
141
+ }
142
+ }
143
+
144
+ if (
145
+ requestInfo.pageType &&
146
+ PageTypeSchema.safeParse(requestInfo.pageType).success
147
+ ) {
148
+ routeContext.pageType = requestInfo.pageType
149
+ }
150
+
151
+ if (
152
+ typeof requestInfo.handle === 'string' &&
153
+ requestInfo.handle &&
154
+ requestInfo.handle.length <= MAX_ROUTE_CONTEXT_HANDLE_LENGTH
155
+ ) {
156
+ routeContext.handle = requestInfo.handle
157
+ }
158
+
159
+ return routeContext
160
+ }
161
+
162
+ /**
163
+ * Studio per-item loader revalidation: POST the unsaved draft item (and, when
164
+ * the runtime exposes route context, a sanitized `routeContext`) to the app's
165
+ * revalidate route (see `createWeaverseNextRevalidateHandler`) and apply the
166
+ * returned `loaderData` to the live item instance in place.
167
+ *
168
+ * No `router.refresh()` is involved, so the RSC tree is never re-applied — no
169
+ * page remount, no scroll reset, and the draft payload never touches the URL.
170
+ *
171
+ * Throws on any failure (missing route, non-OK response, unknown item) so the
172
+ * Builder Studio bridge can detect the miss and fall back to its legacy
173
+ * refresh-based revalidation for this window.
174
+ */
175
+ export async function revalidateWeaverseNextItem(
176
+ runtime: RevalidateItemRuntimeLike,
177
+ draftItem: WeaverseNextComponentData,
178
+ endpoint: string = DEFAULT_REVALIDATE_ENDPOINT
179
+ ): Promise<void> {
180
+ let instance = runtime.itemInstances.get(draftItem.id)
181
+ if (!instance) {
182
+ throw new Error(
183
+ `[weaverse-next] No live item instance for "${draftItem.id}".`
184
+ )
185
+ }
186
+
187
+ // Snapshot route identity at revalidation start so a later navigation cannot
188
+ // retarget this in-flight request. Omit `routeContext` for legacy runtimes.
189
+ let routeContext = buildWeaverseNextRevalidateRouteContext(
190
+ runtime.requestInfo
191
+ )
192
+ let body = routeContext ? { draftItem, routeContext } : { draftItem }
193
+
194
+ let response = await fetch(endpoint, {
195
+ method: 'POST',
196
+ headers: { 'Content-Type': 'application/json' },
197
+ body: JSON.stringify(body),
198
+ })
199
+ if (!response.ok) {
200
+ throw new Error(
201
+ `[weaverse-next] Revalidate endpoint responded ${response.status}.`
202
+ )
203
+ }
204
+
205
+ let { loaderData } = (await response.json()) as { loaderData?: unknown }
206
+ // The item's settings were already mutated by the Studio edit; only the
207
+ // loader payload needs applying. `setData` merges and notifies subscribers,
208
+ // and the new `loaderData` identity is what Builder's revalidation poll
209
+ // observes for completion.
210
+ instance.setData({ loaderData: loaderData ?? null })
211
+ }
@@ -0,0 +1,180 @@
1
+ 'use client'
2
+
3
+ import type { SchemaType } from '@weaverse/schema'
4
+ import {
5
+ createContext,
6
+ type ReactNode,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ } from 'react'
11
+ import {
12
+ getSchemaStaticContent,
13
+ WeaverseNextContext,
14
+ type WeaverseNextContextValue,
15
+ } from './provider'
16
+ import { createWeaverseNextThemeSettingsStore } from './theme-settings-store'
17
+ import type { TranslateFunction } from './translate'
18
+ import { TranslationProvider } from './translation-context'
19
+ import { TranslationStore } from './translation-store'
20
+ import type {
21
+ WeaverseNextThemeSchema,
22
+ WeaverseNextThemeSettingsStore,
23
+ } from './types'
24
+
25
+ /** Root-scoped theme and translation stores adopted by route providers. */
26
+ export interface WeaverseNextRootContextValue {
27
+ /** Active-locale static-text overrides shared with route-level providers. */
28
+ merchantOverrides?: Record<string, unknown>
29
+ /** Theme default locale JSON shared with route-level providers. */
30
+ staticContent: Record<string, unknown>
31
+ /** Root-scoped external store shared with route-level providers. */
32
+ themeSettingsStore: WeaverseNextThemeSettingsStore
33
+ /** Single live design-mode static-text override store for the whole session. */
34
+ translationStore: TranslationStore
35
+ }
36
+
37
+ /**
38
+ * Ambient root-scoped context. Route-level `WeaverseNextProvider` reads this to
39
+ * adopt the shared theme settings store instead of creating its own.
40
+ */
41
+ export const WeaverseNextRootContext =
42
+ createContext<WeaverseNextRootContextValue | null>(null)
43
+
44
+ /** Root theme and translation data shared across the App Router tree. */
45
+ export interface WeaverseNextRootProviderProps {
46
+ /** Application subtree, including global and route-level components. */
47
+ children: ReactNode
48
+ /** Root-loaded theme settings (published-mode fetch, no design-mode context). */
49
+ initialThemeSettings?: Record<string, unknown>
50
+ /** Active-locale static-text overrides from `loadThemeSettings().merchantOverrides`. */
51
+ merchantOverrides?: Record<string, unknown>
52
+ /** Public environment values exposed to the theme-settings store. */
53
+ publicEnv?: Record<string, string | undefined>
54
+ /**
55
+ * Theme default locale JSON (`loadThemeSettings().staticContent`). Falls back
56
+ * to `themeSchema.i18n.staticContent` when omitted.
57
+ */
58
+ staticContent?: Record<string, unknown>
59
+ /** Optional external translation function (host i18n); highest priority in `t()`. */
60
+ t?: TranslateFunction
61
+ /** Theme schema associated with the initial settings. */
62
+ themeSchema?: SchemaType | WeaverseNextThemeSchema
63
+ }
64
+
65
+ /**
66
+ * Root-scoped client boundary that owns the single `WeaverseNextThemeSettingsStore`
67
+ * for the whole app session, matching Hydrogen's `withWeaverse()` root store.
68
+ * Mount once in `app/layout.tsx`, above `Header`/`Footer`/global modules and any
69
+ * route-level `WeaverseNextProvider`.
70
+ *
71
+ * Also renders `WeaverseNextContext` with a minimal value so `useThemeSettings()`
72
+ * works for components mounted directly under this provider, even without a
73
+ * route-level `WeaverseNextProvider` above them.
74
+ *
75
+ * @example
76
+ * ```tsx
77
+ * <WeaverseNextRootProvider initialThemeSettings={theme.theme} themeSchema={theme.schema}>
78
+ * <Header />
79
+ * {children}
80
+ * <Footer />
81
+ * </WeaverseNextRootProvider>
82
+ * ```
83
+ */
84
+ export function WeaverseNextRootProvider(props: WeaverseNextRootProviderProps) {
85
+ let {
86
+ children,
87
+ initialThemeSettings,
88
+ merchantOverrides,
89
+ staticContent,
90
+ t,
91
+ themeSchema,
92
+ publicEnv,
93
+ } = props
94
+
95
+ // `useRef`, not `useMemo`: a parent re-render must never replace this store —
96
+ // it is the single instance Studio's `updateThemeSettings()` mutates in place.
97
+ let storeRef = useRef<WeaverseNextThemeSettingsStore | null>(null)
98
+ if (!storeRef.current) {
99
+ storeRef.current = createWeaverseNextThemeSettingsStore({
100
+ publicEnv,
101
+ schema: themeSchema,
102
+ settings: initialThemeSettings,
103
+ })
104
+ }
105
+ let themeSettingsStore = storeRef.current
106
+
107
+ // Same `useRef` rationale as the theme store: one live static-text store for
108
+ // the whole session, which Builder's `updateStaticText()` RPC mutates in place
109
+ // via `runtime.internal.translationStore`. A parent re-render must never swap
110
+ // it, or design-mode overrides would be lost.
111
+ let translationStoreRef = useRef<TranslationStore | null>(null)
112
+ if (!translationStoreRef.current) {
113
+ translationStoreRef.current = new TranslationStore()
114
+ }
115
+ let translationStore = translationStoreRef.current
116
+
117
+ // If `initialThemeSettings` changes after mount (e.g. root revalidation),
118
+ // update the existing store in place instead of replacing it. This is an
119
+ // effect because external store updates are side effects; the initial render
120
+ // is already seeded synchronously by store construction above.
121
+ let appliedThemeSettingsRef = useRef(initialThemeSettings)
122
+ useEffect(() => {
123
+ if (
124
+ initialThemeSettings &&
125
+ appliedThemeSettingsRef.current !== initialThemeSettings
126
+ ) {
127
+ themeSettingsStore.updateThemeSettings(initialThemeSettings)
128
+ appliedThemeSettingsRef.current = initialThemeSettings
129
+ }
130
+ }, [initialThemeSettings, themeSettingsStore])
131
+
132
+ // Prefer an explicit `staticContent` prop (from `loadThemeSettings()`); fall
133
+ // back to `themeSchema.i18n.staticContent`. Both resolve to referentially
134
+ // stable objects, so memoizing the context values below stays effective.
135
+ let resolvedStaticContent =
136
+ staticContent ?? getSchemaStaticContent(themeSchema)
137
+
138
+ // `themeSettingsStore` / `translationStore` are `useRef`-stable, so memoizing
139
+ // on them (rather than recreating these plain objects every render) keeps
140
+ // `useThemeSettings()` / `useTranslation()` consumers (Header, Footer, ...)
141
+ // from re-rendering on every root re-render.
142
+ let rootContextValue = useMemo<WeaverseNextRootContextValue>(
143
+ () => ({
144
+ merchantOverrides,
145
+ staticContent: resolvedStaticContent,
146
+ themeSettingsStore,
147
+ translationStore,
148
+ }),
149
+ [
150
+ merchantOverrides,
151
+ resolvedStaticContent,
152
+ themeSettingsStore,
153
+ translationStore,
154
+ ]
155
+ )
156
+ let contextValue = useMemo<WeaverseNextContextValue>(
157
+ () => ({
158
+ merchantOverrides,
159
+ themeSettings: themeSettingsStore.getSnapshot(),
160
+ themeSettingsStore,
161
+ translationStore,
162
+ }),
163
+ [merchantOverrides, themeSettingsStore, translationStore]
164
+ )
165
+
166
+ return (
167
+ <WeaverseNextRootContext.Provider value={rootContextValue}>
168
+ <WeaverseNextContext.Provider value={contextValue}>
169
+ <TranslationProvider
170
+ merchantOverrides={merchantOverrides}
171
+ staticContent={resolvedStaticContent}
172
+ t={t}
173
+ translationStore={translationStore}
174
+ >
175
+ {children}
176
+ </TranslationProvider>
177
+ </WeaverseNextContext.Provider>
178
+ </WeaverseNextRootContext.Provider>
179
+ )
180
+ }