@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/item.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { ElementData } from '@weaverse/react'
|
|
2
|
+
import { Weaverse, WeaverseItemStore } from '@weaverse/react'
|
|
3
|
+
import type { WeaverseNextRuntime } from './runtime'
|
|
4
|
+
import type {
|
|
5
|
+
WeaverseNextComponentData,
|
|
6
|
+
WeaverseNextTranslationItemEntry,
|
|
7
|
+
} from './types'
|
|
8
|
+
import { generateDataFromSchema } from './utils'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Spread a serialized item's nested `data` field into top-level props — the
|
|
12
|
+
* shape the shared `@weaverse/react` renderer expects. Top-level fields win
|
|
13
|
+
* over nested ones, matching the constructor's original
|
|
14
|
+
* `Object.assign(store, schemaDefaults, data, rest)` merge order. Flat payloads
|
|
15
|
+
* with no nested `data` pass through unchanged.
|
|
16
|
+
*/
|
|
17
|
+
function flattenItemData(update: Omit<ElementData, 'id' | 'type'>) {
|
|
18
|
+
let { data, ...rest } = update
|
|
19
|
+
if (!(data && typeof data === 'object' && !Array.isArray(data))) {
|
|
20
|
+
return rest
|
|
21
|
+
}
|
|
22
|
+
return { ...data, ...rest }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* While set, item `setData()` queues its subscriber notification here instead
|
|
27
|
+
* of emitting synchronously. `createWeaverseNextRuntime()` runs during React
|
|
28
|
+
* render (the renderer's `useMemo`), and constructing a runtime makes core's
|
|
29
|
+
* `initProject()` call `setData()` on reused item instances (locale/client
|
|
30
|
+
* navigation) — emitting to `useSyncExternalStore` subscribers mid-render
|
|
31
|
+
* trips React's setState-in-render warning. Data is still applied
|
|
32
|
+
* synchronously; only the emit is deferred.
|
|
33
|
+
*/
|
|
34
|
+
let deferredItemUpdates: Set<WeaverseNextItem> | null = null
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Run `create` with item update emissions deferred, returning its result and
|
|
38
|
+
* the items whose `setData()` fired meanwhile. The caller is responsible for
|
|
39
|
+
* calling `triggerUpdate()` on them once React has committed.
|
|
40
|
+
*/
|
|
41
|
+
export function collectDeferredItemUpdates<T>(
|
|
42
|
+
create: () => T
|
|
43
|
+
): [T, WeaverseNextItem[]] {
|
|
44
|
+
let previousQueue = deferredItemUpdates
|
|
45
|
+
let queue = new Set<WeaverseNextItem>()
|
|
46
|
+
deferredItemUpdates = queue
|
|
47
|
+
try {
|
|
48
|
+
return [create(), [...queue]]
|
|
49
|
+
} finally {
|
|
50
|
+
deferredItemUpdates = previousQueue
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Item store that flattens the serialized item's nested `data` field onto the
|
|
56
|
+
* store and seeds schema defaults — same shape Hydrogen produces, so the
|
|
57
|
+
* shared `@weaverse/react` renderer can spread props at the top level.
|
|
58
|
+
*/
|
|
59
|
+
export class WeaverseNextItem extends WeaverseItemStore {
|
|
60
|
+
/** Browser runtime that owns this item store. */
|
|
61
|
+
declare weaverse: WeaverseNextRuntime
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Cached merged snapshot (base `_store` + translations). Reset whenever the
|
|
65
|
+
* `_store` ref or the item's translation entry ref changes, so
|
|
66
|
+
* `useSyncExternalStore` sees a stable object between unrelated re-renders.
|
|
67
|
+
*/
|
|
68
|
+
private _cachedSnapshot: ElementData | null = null
|
|
69
|
+
private _snapshotStoreRef: ElementData | null = null
|
|
70
|
+
private _snapshotTranslationRef: WeaverseNextTranslationItemEntry | null =
|
|
71
|
+
null
|
|
72
|
+
|
|
73
|
+
/** Create an item store from serialized component data and its runtime. */
|
|
74
|
+
constructor(initialData: WeaverseNextComponentData, weaverse: Weaverse) {
|
|
75
|
+
super(initialData as ElementData, weaverse)
|
|
76
|
+
let schemaData = generateDataFromSchema(this.Element?.schema)
|
|
77
|
+
Object.assign(this._store, schemaData, flattenItemData(initialData))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Flatten nested `data` on updates exactly like the constructor does. Core's
|
|
82
|
+
* `initProject()` calls `setData(item)` when a new runtime serves a page
|
|
83
|
+
* whose item instances already exist (e.g. locale/client navigation).
|
|
84
|
+
*
|
|
85
|
+
* A complete serialized item (it carries `id` and `type`; core's
|
|
86
|
+
* `initProject()` always passes one) replaces the store the way the
|
|
87
|
+
* constructor builds it: the schema defaults of the item's own `type` — the
|
|
88
|
+
* Builder can replace a section in place, keeping its id — then the item's
|
|
89
|
+
* settings. A payload omits a setting — or the whole optional `data` — when
|
|
90
|
+
* its value equals the schema default, so merging into the previous store
|
|
91
|
+
* would keep the previous locale's value visible. Partial updates still
|
|
92
|
+
* merge. Either way the `_store` ref changes, which invalidates the memoized
|
|
93
|
+
* snapshot below. The subscriber emit is queued instead of fired when a
|
|
94
|
+
* render-phase deferral is active (see `collectDeferredItemUpdates`).
|
|
95
|
+
*/
|
|
96
|
+
setData = (update: Omit<ElementData, 'id' | 'type'>) => {
|
|
97
|
+
if ('id' in update && 'type' in update) {
|
|
98
|
+
let { data, type } = update as ElementData
|
|
99
|
+
let schema = this.weaverse.elementRegistry.get(type)?.schema
|
|
100
|
+
this._store = {
|
|
101
|
+
...generateDataFromSchema(schema),
|
|
102
|
+
...(data === undefined ? {} : { data }),
|
|
103
|
+
...flattenItemData(update),
|
|
104
|
+
} as ElementData
|
|
105
|
+
} else {
|
|
106
|
+
this.data = flattenItemData(update)
|
|
107
|
+
}
|
|
108
|
+
if (deferredItemUpdates) {
|
|
109
|
+
deferredItemUpdates.add(this)
|
|
110
|
+
} else {
|
|
111
|
+
this.triggerUpdate()
|
|
112
|
+
}
|
|
113
|
+
return this.data
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Overlay item-level translations onto the base store when the runtime is in
|
|
118
|
+
* translation mode. Mirrors Hydrogen's `WeaverseHydrogenItem.getSnapShot()`:
|
|
119
|
+
* memoized by `_store` ref + translation entry ref so the returned object is
|
|
120
|
+
* referentially stable, and a fast path that returns the base store untouched
|
|
121
|
+
* when the item has no translations.
|
|
122
|
+
*/
|
|
123
|
+
getSnapShot = (): ElementData => {
|
|
124
|
+
let base = this._store
|
|
125
|
+
let translations = this.weaverse.translationMap[this._id]
|
|
126
|
+
|
|
127
|
+
// No translations → return the base store directly (fast path).
|
|
128
|
+
if (!translations) {
|
|
129
|
+
return base
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Reuse the cached merge while both the store and translation refs hold.
|
|
133
|
+
if (
|
|
134
|
+
this._cachedSnapshot &&
|
|
135
|
+
this._snapshotStoreRef === base &&
|
|
136
|
+
this._snapshotTranslationRef === translations
|
|
137
|
+
) {
|
|
138
|
+
return this._cachedSnapshot
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let merged = { ...base }
|
|
142
|
+
for (let [key, entry] of Object.entries(translations)) {
|
|
143
|
+
if (entry.translatedValue !== undefined) {
|
|
144
|
+
merged[key] = entry.translatedValue
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this._cachedSnapshot = merged
|
|
149
|
+
this._snapshotStoreRef = base
|
|
150
|
+
this._snapshotTranslationRef = translations
|
|
151
|
+
return merged
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Register the Next item constructor globally. Idempotent: calling it more than
|
|
157
|
+
* once is safe. Invoked by the renderer before it builds a Weaverse instance.
|
|
158
|
+
*/
|
|
159
|
+
export function ensureNextItemConstructor() {
|
|
160
|
+
if (
|
|
161
|
+
!Weaverse.ItemConstructor ||
|
|
162
|
+
Weaverse.ItemConstructor === WeaverseItemStore ||
|
|
163
|
+
Weaverse.ItemConstructor === WeaverseNextItem
|
|
164
|
+
) {
|
|
165
|
+
Weaverse.ItemConstructor = WeaverseNextItem
|
|
166
|
+
}
|
|
167
|
+
}
|
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,223 @@
|
|
|
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 type { TranslateFunction } from './translate'
|
|
14
|
+
import { TranslationProvider } from './translation-context'
|
|
15
|
+
import { TranslationStore } from './translation-store'
|
|
16
|
+
import type {
|
|
17
|
+
WeaverseNextClient,
|
|
18
|
+
WeaverseNextCommerceContext,
|
|
19
|
+
WeaverseNextThemeSettingsStore,
|
|
20
|
+
} from './types'
|
|
21
|
+
|
|
22
|
+
const EMPTY_THEME_SETTINGS: Record<string, unknown> = {}
|
|
23
|
+
const EMPTY_STATIC_CONTENT: Record<string, unknown> = {}
|
|
24
|
+
|
|
25
|
+
/** Read `themeSchema.i18n.staticContent` for the fallback (no-root) provider. */
|
|
26
|
+
export function getSchemaStaticContent(
|
|
27
|
+
schema: unknown
|
|
28
|
+
): Record<string, unknown> {
|
|
29
|
+
let i18n = (schema as { i18n?: { staticContent?: Record<string, unknown> } })
|
|
30
|
+
?.i18n
|
|
31
|
+
return i18n?.staticContent ?? EMPTY_STATIC_CONTENT
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Value carried by {@link WeaverseNextContext}. Holds the explicit data
|
|
36
|
+
* boundaries that replace React Router's `useLoaderData` /
|
|
37
|
+
* `useRouteLoaderData` inside sections.
|
|
38
|
+
*/
|
|
39
|
+
export interface WeaverseNextContextValue {
|
|
40
|
+
/** Client used by the renderer and component-loader compatibility hooks. */
|
|
41
|
+
client?: WeaverseNextClient
|
|
42
|
+
/** App-provided commerce context for rendered components. */
|
|
43
|
+
commerce?: WeaverseNextCommerceContext
|
|
44
|
+
/** Active-locale static-text overrides, threaded into the runtime. */
|
|
45
|
+
merchantOverrides?: Record<string, unknown>
|
|
46
|
+
/** Page-scoped data exposed to rendered components. */
|
|
47
|
+
pageData?: unknown
|
|
48
|
+
/** Root-scoped data exposed to rendered components. */
|
|
49
|
+
rootData?: unknown
|
|
50
|
+
/** Current merged theme-setting values. */
|
|
51
|
+
themeSettings: Record<string, unknown>
|
|
52
|
+
/** External store that publishes live theme-setting updates. */
|
|
53
|
+
themeSettingsStore: WeaverseNextThemeSettingsStore
|
|
54
|
+
/** Live design-mode static-text override store, threaded into the runtime. */
|
|
55
|
+
translationStore: TranslationStore
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Null sentinel default lets hooks detect usage outside the provider and throw
|
|
60
|
+
* a clear error instead of silently returning empty data.
|
|
61
|
+
*/
|
|
62
|
+
export const WeaverseNextContext =
|
|
63
|
+
createContext<WeaverseNextContextValue | null>(null)
|
|
64
|
+
|
|
65
|
+
/** Explicit page, commerce, theme, and translation data for Next components. */
|
|
66
|
+
export interface WeaverseNextProviderProps {
|
|
67
|
+
/** React subtree that consumes the Weaverse Next contexts. */
|
|
68
|
+
children: ReactNode
|
|
69
|
+
/** Client used by the renderer and component-loader compatibility hooks. */
|
|
70
|
+
client?: WeaverseNextClient
|
|
71
|
+
/** App-provided commerce context (storefront/cart/customer). */
|
|
72
|
+
commerce?: WeaverseNextCommerceContext
|
|
73
|
+
/**
|
|
74
|
+
* Active-locale static-text overrides (nested, from `loadThemeSettings()`).
|
|
75
|
+
* Adopts the root provider's `merchantOverrides` when omitted.
|
|
76
|
+
*/
|
|
77
|
+
merchantOverrides?: Record<string, unknown>
|
|
78
|
+
/** Explicit page/route commerce data (replaces section-level `useLoaderData`). */
|
|
79
|
+
pageData?: unknown
|
|
80
|
+
/** Explicit root/global data (replaces `useRouteLoaderData('root')`). */
|
|
81
|
+
rootData?: unknown
|
|
82
|
+
/**
|
|
83
|
+
* Theme default locale JSON (`loadThemeSettings().staticContent`). Adopts the
|
|
84
|
+
* root provider's `staticContent` when omitted, then falls back to
|
|
85
|
+
* `client.themeSchema.i18n.staticContent`.
|
|
86
|
+
*/
|
|
87
|
+
staticContent?: Record<string, unknown>
|
|
88
|
+
/** Optional external translation function (host i18n); highest priority in `t()`. */
|
|
89
|
+
t?: TranslateFunction
|
|
90
|
+
/** Theme settings override; falls back to `client.themeSettings`. */
|
|
91
|
+
themeSettings?: Record<string, unknown>
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Client boundary that supplies explicit root/page/commerce data to Weaverse
|
|
96
|
+
* sections. Next-native replacement for `withWeaverse` + route loader data —
|
|
97
|
+
* no React Router route matches are emulated.
|
|
98
|
+
*
|
|
99
|
+
* In App Router, create/pass `client` only from a client component wrapper. A
|
|
100
|
+
* `WeaverseNextClient` contains functions and component references, so it is
|
|
101
|
+
* not serializable across a Server Component → Client Component boundary.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```tsx
|
|
105
|
+
* 'use client'
|
|
106
|
+
*
|
|
107
|
+
* <WeaverseNextProvider client={client} rootData={rootData} pageData={pageData} commerce={commerce}>
|
|
108
|
+
* <WeaverseNextRenderer />
|
|
109
|
+
* </WeaverseNextProvider>
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export function WeaverseNextProvider(props: WeaverseNextProviderProps) {
|
|
113
|
+
let { children, client, rootData, pageData, commerce, themeSettings } = props
|
|
114
|
+
let rootContext = useContext(WeaverseNextRootContext)
|
|
115
|
+
|
|
116
|
+
let themeSettingsValue =
|
|
117
|
+
themeSettings ?? client?.themeSettings ?? EMPTY_THEME_SETTINGS
|
|
118
|
+
|
|
119
|
+
// Fallback translation store when no `WeaverseNextRootProvider` is mounted.
|
|
120
|
+
// Otherwise the ambient root store is adopted, so exactly one live static-text
|
|
121
|
+
// store backs Builder's `updateStaticText()` RPC per page load — mirroring the
|
|
122
|
+
// theme-settings store adoption below. `useRef`, not `useMemo`: React may
|
|
123
|
+
// evict memoized values, but this store must survive every re-render so
|
|
124
|
+
// design-mode overrides already applied by Builder are never dropped.
|
|
125
|
+
let ownTranslationStoreRef = useRef<TranslationStore | null>(null)
|
|
126
|
+
if (!(rootContext || ownTranslationStoreRef.current)) {
|
|
127
|
+
ownTranslationStoreRef.current = new TranslationStore()
|
|
128
|
+
}
|
|
129
|
+
let translationStore =
|
|
130
|
+
rootContext?.translationStore ??
|
|
131
|
+
(ownTranslationStoreRef.current as TranslationStore)
|
|
132
|
+
|
|
133
|
+
// Static content / merchant overrides feeding `useTranslation()`. Precedence:
|
|
134
|
+
// explicit prop → ambient root value → (static content only) schema default.
|
|
135
|
+
let staticContent =
|
|
136
|
+
props.staticContent ??
|
|
137
|
+
rootContext?.staticContent ??
|
|
138
|
+
getSchemaStaticContent(client?.themeSchema)
|
|
139
|
+
let merchantOverrides =
|
|
140
|
+
props.merchantOverrides ?? rootContext?.merchantOverrides
|
|
141
|
+
|
|
142
|
+
// Fallback store for apps that don't mount `WeaverseNextRootProvider`. Not
|
|
143
|
+
// created at all when a root store is present, so exactly one
|
|
144
|
+
// `WeaverseNextThemeSettingsStore` exists per page load either way.
|
|
145
|
+
let ownThemeSettingsStore = useMemo(() => {
|
|
146
|
+
if (rootContext) {
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
return createWeaverseNextThemeSettingsStore({
|
|
150
|
+
schema: client?.themeSchema,
|
|
151
|
+
settings: themeSettingsValue,
|
|
152
|
+
})
|
|
153
|
+
}, [rootContext, client?.themeSchema, themeSettingsValue])
|
|
154
|
+
|
|
155
|
+
let themeSettingsStore =
|
|
156
|
+
rootContext?.themeSettingsStore ??
|
|
157
|
+
(ownThemeSettingsStore as WeaverseNextThemeSettingsStore)
|
|
158
|
+
|
|
159
|
+
// When adopting the root store, route-supplied theme data augments it in
|
|
160
|
+
// place (merge), never replaces it — Studio and root `Header`/`Footer` must
|
|
161
|
+
// keep observing the same instance. Guarded so a stable `themeSettingsValue`
|
|
162
|
+
// across re-renders doesn't re-merge/re-notify every render.
|
|
163
|
+
//
|
|
164
|
+
// This runs in an effect, not render: mutating a store shared outside this
|
|
165
|
+
// component's subtree is a side effect, and render functions must stay
|
|
166
|
+
// pure under concurrent React (retried/discarded renders, StrictMode
|
|
167
|
+
// double-invocation). The root's initial theme is authoritative for SSR;
|
|
168
|
+
// a route's `themeSettings`/`client.themeSettings` override applies after
|
|
169
|
+
// client mount instead of racing a render-phase store mutation. Apps that
|
|
170
|
+
// don't mount `WeaverseNextRootProvider` are unaffected — `themeSettings`
|
|
171
|
+
// still renders synchronously on SSR via `ownThemeSettingsStore` below.
|
|
172
|
+
let mergedIntoRootRef = useRef<Record<string, unknown> | undefined>(undefined)
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
if (
|
|
175
|
+
rootContext &&
|
|
176
|
+
themeSettingsValue !== EMPTY_THEME_SETTINGS &&
|
|
177
|
+
mergedIntoRootRef.current !== themeSettingsValue
|
|
178
|
+
) {
|
|
179
|
+
rootContext.themeSettingsStore.updateThemeSettings(themeSettingsValue)
|
|
180
|
+
mergedIntoRootRef.current = themeSettingsValue
|
|
181
|
+
}
|
|
182
|
+
}, [rootContext, themeSettingsValue])
|
|
183
|
+
|
|
184
|
+
// Do not depend on `themeSettingsValue` here. In fallback mode it is already
|
|
185
|
+
// represented by a new `themeSettingsStore`; in root-adoption mode the
|
|
186
|
+
// post-mount merge effect notifies `useThemeSettings()` subscribers. Including
|
|
187
|
+
// it would recreate this context with a stale root snapshot before the effect
|
|
188
|
+
// fires, causing an avoidable extra render.
|
|
189
|
+
let value = useMemo<WeaverseNextContextValue>(
|
|
190
|
+
() => ({
|
|
191
|
+
client,
|
|
192
|
+
rootData,
|
|
193
|
+
pageData,
|
|
194
|
+
commerce: commerce ?? client?.commerce,
|
|
195
|
+
merchantOverrides,
|
|
196
|
+
themeSettings: themeSettingsStore.getSnapshot(),
|
|
197
|
+
themeSettingsStore,
|
|
198
|
+
translationStore,
|
|
199
|
+
}),
|
|
200
|
+
[
|
|
201
|
+
client,
|
|
202
|
+
rootData,
|
|
203
|
+
pageData,
|
|
204
|
+
commerce,
|
|
205
|
+
merchantOverrides,
|
|
206
|
+
themeSettingsStore,
|
|
207
|
+
translationStore,
|
|
208
|
+
]
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<WeaverseNextContext.Provider value={value}>
|
|
213
|
+
<TranslationProvider
|
|
214
|
+
merchantOverrides={merchantOverrides}
|
|
215
|
+
staticContent={staticContent}
|
|
216
|
+
t={props.t}
|
|
217
|
+
translationStore={translationStore}
|
|
218
|
+
>
|
|
219
|
+
{children}
|
|
220
|
+
</TranslationProvider>
|
|
221
|
+
</WeaverseNextContext.Provider>
|
|
222
|
+
)
|
|
223
|
+
}
|
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
|
+
}
|