@weaverse/next 0.1.0-alpha.2 → 0.1.0-alpha.20
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 +586 -123
- package/dist/chunk-LH6AAEIK.mjs +326 -0
- package/dist/chunk-LH6AAEIK.mjs.map +1 -0
- package/dist/index.js +784 -60
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +588 -58
- package/dist/index.mjs.map +1 -1
- package/dist/server.js +537 -76
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +363 -27
- package/dist/server.mjs.map +1 -1
- 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 +34 -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/translation-context.d.ts +156 -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 +12 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/package.json +12 -8
- package/src/client.ts +102 -0
- package/src/hooks.ts +50 -0
- package/src/index.ts +153 -0
- package/src/item.ts +167 -0
- package/src/loader.ts +135 -0
- package/src/main.tsx +45 -0
- package/src/provider.tsx +225 -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 +182 -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/translation-context.tsx +299 -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 +37 -0
- package/dist/chunk-SS6QUKYY.mjs +0 -149
- package/dist/chunk-SS6QUKYY.mjs.map +0 -1
- package/dist/index.d.mts +0 -250
- package/dist/index.d.ts +0 -250
- package/dist/server.d.mts +0 -60
- package/dist/server.d.ts +0 -60
- package/dist/types-DvEeLX6L.d.mts +0 -321
- package/dist/types-DvEeLX6L.d.ts +0 -321
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
|
+
}
|
package/src/provider.tsx
ADDED
|
@@ -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
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -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
|
+
}
|
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
|
+
}
|