@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.
- package/README.md +161 -73
- package/dist/{chunk-6PMZ77RA.mjs → chunk-OSC5HR5V.mjs} +117 -8
- package/dist/chunk-OSC5HR5V.mjs.map +1 -0
- package/dist/client-boundary.js +1425 -0
- package/dist/client-boundary.js.map +1 -0
- package/dist/client-boundary.mjs +1395 -0
- package/dist/client-boundary.mjs.map +1 -0
- package/dist/index.js +292 -1003
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +90 -966
- package/dist/index.mjs.map +1 -1
- package/dist/server.js +321 -91
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +205 -28
- package/dist/server.mjs.map +1 -1
- package/dist/types/client-boundary.d.ts +25 -0
- package/dist/types/client-boundary.d.ts.map +1 -0
- package/dist/types/client.d.ts +16 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/hooks.d.ts +20 -0
- package/dist/types/hooks.d.ts.map +1 -0
- package/dist/types/index.d.ts +33 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/item.d.ts +60 -0
- package/dist/types/item.d.ts.map +1 -0
- package/dist/types/loader.d.ts +25 -0
- package/dist/types/loader.d.ts.map +1 -0
- package/dist/types/main.d.ts +11 -0
- package/dist/types/main.d.ts.map +1 -0
- package/dist/types/provider.d.ts +82 -0
- package/dist/types/provider.d.ts.map +1 -0
- package/dist/types/registry.d.ts +15 -0
- package/dist/types/registry.d.ts.map +1 -0
- package/dist/types/renderer.d.ts +21 -0
- package/dist/types/renderer.d.ts.map +1 -0
- package/dist/types/request-info.d.ts +19 -0
- package/dist/types/request-info.d.ts.map +1 -0
- package/dist/types/revalidate-item.d.ts +82 -0
- package/dist/types/revalidate-item.d.ts.map +1 -0
- package/dist/types/root-provider.d.ts +62 -0
- package/dist/types/root-provider.d.ts.map +1 -0
- package/dist/types/runtime.d.ts +113 -0
- package/dist/types/runtime.d.ts.map +1 -0
- package/dist/types/seo.d.ts +55 -0
- package/dist/types/seo.d.ts.map +1 -0
- package/dist/types/server/configs.d.ts +27 -0
- package/dist/types/server/configs.d.ts.map +1 -0
- package/dist/types/server/normalize-page-url.d.ts +19 -0
- package/dist/types/server/normalize-page-url.d.ts.map +1 -0
- package/dist/types/server/revalidate-handler.d.ts +55 -0
- package/dist/types/server/revalidate-handler.d.ts.map +1 -0
- package/dist/types/server/server-client.d.ts +21 -0
- package/dist/types/server/server-client.d.ts.map +1 -0
- package/dist/types/server.d.ts +19 -0
- package/dist/types/server.d.ts.map +1 -0
- package/dist/types/studio-bridge.d.ts +16 -0
- package/dist/types/studio-bridge.d.ts.map +1 -0
- package/dist/types/studio-connect.d.ts +27 -0
- package/dist/types/studio-connect.d.ts.map +1 -0
- package/dist/types/studio-router.d.ts +50 -0
- package/dist/types/studio-router.d.ts.map +1 -0
- package/dist/types/studio-script-src.d.ts +12 -0
- package/dist/types/studio-script-src.d.ts.map +1 -0
- package/dist/types/theme-settings-store.d.ts +18 -0
- package/dist/types/theme-settings-store.d.ts.map +1 -0
- package/dist/types/translate.d.ts +56 -0
- package/dist/types/translate.d.ts.map +1 -0
- package/dist/types/translation-context.d.ts +109 -0
- package/dist/types/translation-context.d.ts.map +1 -0
- package/dist/types/translation-store.d.ts +44 -0
- package/dist/types/translation-store.d.ts.map +1 -0
- package/dist/types/types.d.ts +665 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/use-weaverse-next-studio.d.ts +34 -0
- package/dist/types/use-weaverse-next-studio.d.ts.map +1 -0
- package/dist/types/utils.d.ts +22 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/package.json +12 -27
- package/src/client-boundary.ts +50 -0
- package/src/client.ts +102 -0
- package/src/hooks.ts +50 -0
- package/src/index.ts +143 -0
- package/src/item.ts +167 -0
- package/src/loader.ts +135 -0
- package/src/main.tsx +45 -0
- package/src/provider.tsx +223 -0
- package/src/registry.ts +63 -0
- package/src/renderer.tsx +100 -0
- package/src/request-info.ts +124 -0
- package/src/revalidate-item.ts +211 -0
- package/src/root-provider.tsx +180 -0
- package/src/runtime.ts +555 -0
- package/src/seo.ts +157 -0
- package/src/server/configs.ts +178 -0
- package/src/server/normalize-page-url.ts +73 -0
- package/src/server/revalidate-handler.ts +357 -0
- package/src/server/server-client.ts +728 -0
- package/src/server.ts +67 -0
- package/src/studio-bridge.tsx +82 -0
- package/src/studio-connect.tsx +88 -0
- package/src/studio-router.ts +85 -0
- package/src/studio-script-src.ts +128 -0
- package/src/theme-settings-store.ts +47 -0
- package/src/translate.ts +125 -0
- package/src/translation-context.tsx +182 -0
- package/src/translation-store.ts +69 -0
- package/src/types.ts +753 -0
- package/src/use-weaverse-next-studio.tsx +106 -0
- package/src/utils.ts +49 -0
- package/dist/chunk-6PMZ77RA.mjs.map +0 -1
- package/dist/index.d.mts +0 -580
- package/dist/index.d.ts +0 -580
- package/dist/seo-Dos9t22_.d.mts +0 -477
- package/dist/seo-Dos9t22_.d.ts +0 -477
- package/dist/server.d.mts +0 -99
- package/dist/server.d.ts +0 -99
package/src/renderer.tsx
ADDED
|
@@ -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
|
+
}
|