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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +125 -23
  2. package/dist/{chunk-MQ6N5ODA.mjs → chunk-NL22OVD7.mjs} +93 -4
  3. package/dist/chunk-NL22OVD7.mjs.map +1 -0
  4. package/dist/index.js +85 -4
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +19 -26
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/server.js +158 -2
  9. package/dist/server.js.map +1 -1
  10. package/dist/server.mjs +126 -5
  11. package/dist/server.mjs.map +1 -1
  12. package/dist/types/client.d.ts +16 -0
  13. package/dist/types/client.d.ts.map +1 -0
  14. package/dist/types/hooks.d.ts +20 -0
  15. package/dist/types/hooks.d.ts.map +1 -0
  16. package/dist/types/index.d.ts +34 -0
  17. package/dist/types/index.d.ts.map +1 -0
  18. package/dist/types/item.d.ts +54 -0
  19. package/dist/types/item.d.ts.map +1 -0
  20. package/dist/types/loader.d.ts +25 -0
  21. package/dist/types/loader.d.ts.map +1 -0
  22. package/dist/types/main.d.ts +11 -0
  23. package/dist/types/main.d.ts.map +1 -0
  24. package/dist/types/provider.d.ts +82 -0
  25. package/dist/types/provider.d.ts.map +1 -0
  26. package/dist/types/registry.d.ts +15 -0
  27. package/dist/types/registry.d.ts.map +1 -0
  28. package/dist/types/renderer.d.ts +20 -0
  29. package/dist/types/renderer.d.ts.map +1 -0
  30. package/dist/types/request-info.d.ts +8 -0
  31. package/dist/types/request-info.d.ts.map +1 -0
  32. package/dist/types/revalidate-item.d.ts +81 -0
  33. package/dist/types/revalidate-item.d.ts.map +1 -0
  34. package/dist/types/root-provider.d.ts +61 -0
  35. package/dist/types/root-provider.d.ts.map +1 -0
  36. package/dist/types/runtime.d.ts +113 -0
  37. package/dist/types/runtime.d.ts.map +1 -0
  38. package/dist/types/seo.d.ts +55 -0
  39. package/dist/types/seo.d.ts.map +1 -0
  40. package/dist/types/server/configs.d.ts +27 -0
  41. package/dist/types/server/configs.d.ts.map +1 -0
  42. package/dist/types/server/normalize-page-url.d.ts +19 -0
  43. package/dist/types/server/normalize-page-url.d.ts.map +1 -0
  44. package/dist/types/server/revalidate-handler.d.ts +55 -0
  45. package/dist/types/server/revalidate-handler.d.ts.map +1 -0
  46. package/dist/types/server/server-client.d.ts +21 -0
  47. package/dist/types/server/server-client.d.ts.map +1 -0
  48. package/dist/types/server.d.ts +19 -0
  49. package/dist/types/server.d.ts.map +1 -0
  50. package/dist/types/studio-bridge.d.ts +16 -0
  51. package/dist/types/studio-bridge.d.ts.map +1 -0
  52. package/dist/types/studio-connect.d.ts +27 -0
  53. package/dist/types/studio-connect.d.ts.map +1 -0
  54. package/dist/types/studio-router.d.ts +50 -0
  55. package/dist/types/studio-router.d.ts.map +1 -0
  56. package/dist/types/studio-script-src.d.ts +12 -0
  57. package/dist/types/studio-script-src.d.ts.map +1 -0
  58. package/dist/types/theme-settings-store.d.ts +17 -0
  59. package/dist/types/theme-settings-store.d.ts.map +1 -0
  60. package/dist/types/translation-context.d.ts +156 -0
  61. package/dist/types/translation-context.d.ts.map +1 -0
  62. package/dist/types/translation-store.d.ts +44 -0
  63. package/dist/types/translation-store.d.ts.map +1 -0
  64. package/dist/types/types.d.ts +661 -0
  65. package/dist/types/types.d.ts.map +1 -0
  66. package/dist/types/use-weaverse-next-studio.d.ts +34 -0
  67. package/dist/types/use-weaverse-next-studio.d.ts.map +1 -0
  68. package/dist/types/utils.d.ts +12 -0
  69. package/dist/types/utils.d.ts.map +1 -0
  70. package/package.json +7 -6
  71. package/src/client.ts +101 -0
  72. package/src/hooks.ts +50 -0
  73. package/src/index.ts +153 -0
  74. package/src/item.ts +151 -0
  75. package/src/loader.ts +135 -0
  76. package/src/main.tsx +45 -0
  77. package/src/provider.tsx +225 -0
  78. package/src/registry.ts +63 -0
  79. package/src/renderer.tsx +99 -0
  80. package/src/request-info.ts +95 -0
  81. package/src/revalidate-item.ts +210 -0
  82. package/src/root-provider.tsx +181 -0
  83. package/src/runtime.ts +521 -0
  84. package/src/seo.ts +157 -0
  85. package/src/server/configs.ts +182 -0
  86. package/src/server/normalize-page-url.ts +65 -0
  87. package/src/server/revalidate-handler.ts +357 -0
  88. package/src/server/server-client.ts +727 -0
  89. package/src/server.ts +67 -0
  90. package/src/studio-bridge.tsx +82 -0
  91. package/src/studio-connect.tsx +88 -0
  92. package/src/studio-router.ts +85 -0
  93. package/src/studio-script-src.ts +125 -0
  94. package/src/theme-settings-store.ts +46 -0
  95. package/src/translation-context.tsx +299 -0
  96. package/src/translation-store.ts +69 -0
  97. package/src/types.ts +748 -0
  98. package/src/use-weaverse-next-studio.tsx +106 -0
  99. package/src/utils.ts +37 -0
  100. package/dist/chunk-MQ6N5ODA.mjs.map +0 -1
  101. package/dist/index.d.mts +0 -592
  102. package/dist/index.d.ts +0 -592
  103. package/dist/seo-CrEFl_Up.d.mts +0 -516
  104. package/dist/seo-CrEFl_Up.d.ts +0 -516
  105. package/dist/server.d.mts +0 -100
  106. package/dist/server.d.ts +0 -100
@@ -0,0 +1,210 @@
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=`/`?weaverseApiKey=`
21
+ * must never influence server config resolution (see `getWeaverseNextConfigs`).
22
+ * - Transient: draft-item and RSC transport params never describe route identity.
23
+ */
24
+ export const DENIED_ROUTE_CONTEXT_PARAMS = [
25
+ 'weaverseprojectid',
26
+ 'weaversehost',
27
+ 'weaverseapikey',
28
+ 'weaverseapibase',
29
+ 'weaversepublicapibase',
30
+ 'weaverseversion',
31
+ 'projectid',
32
+ 'weaversedraftitem',
33
+ '__weaversedraftitem',
34
+ '_rsc',
35
+ ]
36
+
37
+ /** i18n fields allowed to cross the browser boundary. */
38
+ export const ROUTE_CONTEXT_I18N_KEYS = [
39
+ 'country',
40
+ 'label',
41
+ 'language',
42
+ 'locale',
43
+ 'pathPrefix',
44
+ ] as const
45
+
46
+ /**
47
+ * JSON-safe route subset that may cross the browser boundary alongside the
48
+ * draft item. Never carries headers, cookies, auth, env, project ID, Studio
49
+ * host, API keys/bases, commerce clients, the request context, runtime, or
50
+ * client. `search` is canonical (preserves duplicate keys); the derived
51
+ * `queries` record is intentionally omitted.
52
+ */
53
+ export interface WeaverseNextRevalidateRouteContext {
54
+ /** Route handle used for page assignment. */
55
+ handle?: string
56
+ /** Public locale and market identity allowed across the browser boundary. */
57
+ i18n?: Pick<
58
+ WeaverseNextI18n,
59
+ 'country' | 'label' | 'language' | 'locale' | 'pathPrefix'
60
+ >
61
+ /** Weaverse page type resolved by the active route. */
62
+ pageType?: PageType
63
+ /** Canonical same-origin route pathname. */
64
+ pathname: string
65
+ /** Sanitized serialized query string, including the leading `?` when set. */
66
+ search: string
67
+ }
68
+
69
+ /**
70
+ * Minimal runtime surface the in-place apply needs. Structural (not the
71
+ * concrete `WeaverseNextRuntime` class) so unit tests can drive it with a
72
+ * plain mock. `requestInfo` stays optional: a real `WeaverseNextRuntime`
73
+ * always supplies it, but when absent the client omits `routeContext` and the
74
+ * legacy `{ draftItem }` request is preserved.
75
+ */
76
+ export interface RevalidateItemRuntimeLike {
77
+ /** Live item stores keyed by Weaverse item ID. */
78
+ itemInstances: Map<
79
+ string,
80
+ {
81
+ /** Merge refreshed loader data into the live item store. */
82
+ setData: (update: Record<string, unknown>) => unknown
83
+ }
84
+ >
85
+ /** Active route identity used to construct the optional revalidation payload. */
86
+ requestInfo?: WeaverseNextRequestInfo
87
+ }
88
+
89
+ /**
90
+ * Strip denied (server-owned/transient) controls from a `search` string and
91
+ * re-serialize it. Preserves ordinary application queries, duplicate keys,
92
+ * encoded Unicode, and mode controls (`isDesignMode`, `isPreviewMode`,
93
+ * `__revisionId`, `sectionType`). Returns `''` or a `?`-prefixed string.
94
+ */
95
+ export function sanitizeRouteContextSearch(search: string): string {
96
+ let params = new URLSearchParams(search)
97
+ let denied = new Set(DENIED_ROUTE_CONTEXT_PARAMS)
98
+ for (let key of [...new Set(params.keys())]) {
99
+ if (denied.has(key.toLowerCase())) {
100
+ params.delete(key)
101
+ }
102
+ }
103
+ let serialized = params.toString()
104
+ return serialized ? `?${serialized}` : ''
105
+ }
106
+
107
+ /**
108
+ * Build the sanitized {@link WeaverseNextRevalidateRouteContext} from the
109
+ * active runtime's request info. Returns `undefined` without a usable pathname
110
+ * so the caller omits `routeContext` and keeps the legacy request. This is not
111
+ * the security boundary — the route handler re-validates and re-sanitizes.
112
+ */
113
+ export function buildWeaverseNextRevalidateRouteContext(
114
+ requestInfo?: WeaverseNextRequestInfo
115
+ ): WeaverseNextRevalidateRouteContext | undefined {
116
+ if (
117
+ !requestInfo ||
118
+ typeof requestInfo.pathname !== 'string' ||
119
+ !requestInfo.pathname
120
+ ) {
121
+ return
122
+ }
123
+
124
+ let routeContext: WeaverseNextRevalidateRouteContext = {
125
+ pathname: requestInfo.pathname,
126
+ search: sanitizeRouteContextSearch(requestInfo.search ?? ''),
127
+ }
128
+
129
+ let { i18n } = requestInfo
130
+ if (i18n && typeof i18n === 'object') {
131
+ let picked: Record<string, string> = {}
132
+ for (let key of ROUTE_CONTEXT_I18N_KEYS) {
133
+ let value = i18n[key]
134
+ if (typeof value === 'string') {
135
+ picked[key] = value
136
+ }
137
+ }
138
+ if (Object.keys(picked).length) {
139
+ routeContext.i18n = picked
140
+ }
141
+ }
142
+
143
+ if (
144
+ requestInfo.pageType &&
145
+ PageTypeSchema.safeParse(requestInfo.pageType).success
146
+ ) {
147
+ routeContext.pageType = requestInfo.pageType
148
+ }
149
+
150
+ if (
151
+ typeof requestInfo.handle === 'string' &&
152
+ requestInfo.handle &&
153
+ requestInfo.handle.length <= MAX_ROUTE_CONTEXT_HANDLE_LENGTH
154
+ ) {
155
+ routeContext.handle = requestInfo.handle
156
+ }
157
+
158
+ return routeContext
159
+ }
160
+
161
+ /**
162
+ * Studio per-item loader revalidation: POST the unsaved draft item (and, when
163
+ * the runtime exposes route context, a sanitized `routeContext`) to the app's
164
+ * revalidate route (see `createWeaverseNextRevalidateHandler`) and apply the
165
+ * returned `loaderData` to the live item instance in place.
166
+ *
167
+ * No `router.refresh()` is involved, so the RSC tree is never re-applied — no
168
+ * page remount, no scroll reset, and the draft payload never touches the URL.
169
+ *
170
+ * Throws on any failure (missing route, non-OK response, unknown item) so the
171
+ * Builder Studio bridge can detect the miss and fall back to its legacy
172
+ * refresh-based revalidation for this window.
173
+ */
174
+ export async function revalidateWeaverseNextItem(
175
+ runtime: RevalidateItemRuntimeLike,
176
+ draftItem: WeaverseNextComponentData,
177
+ endpoint: string = DEFAULT_REVALIDATE_ENDPOINT
178
+ ): Promise<void> {
179
+ let instance = runtime.itemInstances.get(draftItem.id)
180
+ if (!instance) {
181
+ throw new Error(
182
+ `[weaverse-next] No live item instance for "${draftItem.id}".`
183
+ )
184
+ }
185
+
186
+ // Snapshot route identity at revalidation start so a later navigation cannot
187
+ // retarget this in-flight request. Omit `routeContext` for legacy runtimes.
188
+ let routeContext = buildWeaverseNextRevalidateRouteContext(
189
+ runtime.requestInfo
190
+ )
191
+ let body = routeContext ? { draftItem, routeContext } : { draftItem }
192
+
193
+ let response = await fetch(endpoint, {
194
+ method: 'POST',
195
+ headers: { 'Content-Type': 'application/json' },
196
+ body: JSON.stringify(body),
197
+ })
198
+ if (!response.ok) {
199
+ throw new Error(
200
+ `[weaverse-next] Revalidate endpoint responded ${response.status}.`
201
+ )
202
+ }
203
+
204
+ let { loaderData } = (await response.json()) as { loaderData?: unknown }
205
+ // The item's settings were already mutated by the Studio edit; only the
206
+ // loader payload needs applying. `setData` merges and notifies subscribers,
207
+ // and the new `loaderData` identity is what Builder's revalidation poll
208
+ // observes for completion.
209
+ instance.setData({ loaderData: loaderData ?? null })
210
+ }
@@ -0,0 +1,181 @@
1
+ 'use client'
2
+
3
+ import {
4
+ createContext,
5
+ type ReactNode,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ } from 'react'
10
+ import {
11
+ getSchemaStaticContent,
12
+ WeaverseNextContext,
13
+ type WeaverseNextContextValue,
14
+ } from './provider'
15
+ import { createWeaverseNextThemeSettingsStore } from './theme-settings-store'
16
+ import {
17
+ type TranslateFunction,
18
+ TranslationProvider,
19
+ } from './translation-context'
20
+ import { TranslationStore } from './translation-store'
21
+ import type {
22
+ WeaverseNextThemeSchema,
23
+ WeaverseNextThemeSettingsStore,
24
+ } from './types'
25
+
26
+ /** Root-scoped theme and translation stores adopted by route providers. */
27
+ export interface WeaverseNextRootContextValue {
28
+ /** Active-locale static-text overrides shared with route-level providers. */
29
+ merchantOverrides?: Record<string, unknown>
30
+ /** Theme default locale JSON shared with route-level providers. */
31
+ staticContent: Record<string, unknown>
32
+ /** Root-scoped external store shared with route-level providers. */
33
+ themeSettingsStore: WeaverseNextThemeSettingsStore
34
+ /** Single live design-mode static-text override store for the whole session. */
35
+ translationStore: TranslationStore
36
+ }
37
+
38
+ /**
39
+ * Ambient root-scoped context. Route-level `WeaverseNextProvider` reads this to
40
+ * adopt the shared theme settings store instead of creating its own.
41
+ */
42
+ export const WeaverseNextRootContext =
43
+ createContext<WeaverseNextRootContextValue | null>(null)
44
+
45
+ /** Root theme and translation data shared across the App Router tree. */
46
+ export interface WeaverseNextRootProviderProps {
47
+ /** Application subtree, including global and route-level components. */
48
+ children: ReactNode
49
+ /** Root-loaded theme settings (published-mode fetch, no design-mode context). */
50
+ initialThemeSettings?: Record<string, unknown>
51
+ /** Active-locale static-text overrides from `loadThemeSettings().merchantOverrides`. */
52
+ merchantOverrides?: Record<string, unknown>
53
+ /** Public environment values exposed to the theme-settings store. */
54
+ publicEnv?: Record<string, string | undefined>
55
+ /**
56
+ * Theme default locale JSON (`loadThemeSettings().staticContent`). Falls back
57
+ * to `themeSchema.i18n.staticContent` when omitted.
58
+ */
59
+ staticContent?: Record<string, unknown>
60
+ /** Optional external translation function (host i18n); highest priority in `t()`. */
61
+ t?: TranslateFunction
62
+ /** Theme schema associated with the initial settings. */
63
+ themeSchema?: WeaverseNextThemeSchema
64
+ }
65
+
66
+ /**
67
+ * Root-scoped client boundary that owns the single `WeaverseNextThemeSettingsStore`
68
+ * for the whole app session, matching Hydrogen's `withWeaverse()` root store.
69
+ * Mount once in `app/layout.tsx`, above `Header`/`Footer`/global modules and any
70
+ * route-level `WeaverseNextProvider`.
71
+ *
72
+ * Also renders `WeaverseNextContext` with a minimal value so `useThemeSettings()`
73
+ * works for components mounted directly under this provider, even without a
74
+ * route-level `WeaverseNextProvider` above them.
75
+ *
76
+ * @example
77
+ * ```tsx
78
+ * <WeaverseNextRootProvider initialThemeSettings={theme.theme} themeSchema={theme.schema}>
79
+ * <Header />
80
+ * {children}
81
+ * <Footer />
82
+ * </WeaverseNextRootProvider>
83
+ * ```
84
+ */
85
+ export function WeaverseNextRootProvider(props: WeaverseNextRootProviderProps) {
86
+ let {
87
+ children,
88
+ initialThemeSettings,
89
+ merchantOverrides,
90
+ staticContent,
91
+ t,
92
+ themeSchema,
93
+ publicEnv,
94
+ } = props
95
+
96
+ // `useRef`, not `useMemo`: a parent re-render must never replace this store —
97
+ // it is the single instance Studio's `updateThemeSettings()` mutates in place.
98
+ let storeRef = useRef<WeaverseNextThemeSettingsStore | null>(null)
99
+ if (!storeRef.current) {
100
+ storeRef.current = createWeaverseNextThemeSettingsStore({
101
+ publicEnv,
102
+ schema: themeSchema,
103
+ settings: initialThemeSettings,
104
+ })
105
+ }
106
+ let themeSettingsStore = storeRef.current
107
+
108
+ // Same `useRef` rationale as the theme store: one live static-text store for
109
+ // the whole session, which Builder's `updateStaticText()` RPC mutates in place
110
+ // via `runtime.internal.translationStore`. A parent re-render must never swap
111
+ // it, or design-mode overrides would be lost.
112
+ let translationStoreRef = useRef<TranslationStore | null>(null)
113
+ if (!translationStoreRef.current) {
114
+ translationStoreRef.current = new TranslationStore()
115
+ }
116
+ let translationStore = translationStoreRef.current
117
+
118
+ // If `initialThemeSettings` changes after mount (e.g. root revalidation),
119
+ // update the existing store in place instead of replacing it. This is an
120
+ // effect because external store updates are side effects; the initial render
121
+ // is already seeded synchronously by store construction above.
122
+ let appliedThemeSettingsRef = useRef(initialThemeSettings)
123
+ useEffect(() => {
124
+ if (
125
+ initialThemeSettings &&
126
+ appliedThemeSettingsRef.current !== initialThemeSettings
127
+ ) {
128
+ themeSettingsStore.updateThemeSettings(initialThemeSettings)
129
+ appliedThemeSettingsRef.current = initialThemeSettings
130
+ }
131
+ }, [initialThemeSettings, themeSettingsStore])
132
+
133
+ // Prefer an explicit `staticContent` prop (from `loadThemeSettings()`); fall
134
+ // back to `themeSchema.i18n.staticContent`. Both resolve to referentially
135
+ // stable objects, so memoizing the context values below stays effective.
136
+ let resolvedStaticContent =
137
+ staticContent ?? getSchemaStaticContent(themeSchema)
138
+
139
+ // `themeSettingsStore` / `translationStore` are `useRef`-stable, so memoizing
140
+ // on them (rather than recreating these plain objects every render) keeps
141
+ // `useThemeSettings()` / `useTranslation()` consumers (Header, Footer, ...)
142
+ // from re-rendering on every root re-render.
143
+ let rootContextValue = useMemo<WeaverseNextRootContextValue>(
144
+ () => ({
145
+ merchantOverrides,
146
+ staticContent: resolvedStaticContent,
147
+ themeSettingsStore,
148
+ translationStore,
149
+ }),
150
+ [
151
+ merchantOverrides,
152
+ resolvedStaticContent,
153
+ themeSettingsStore,
154
+ translationStore,
155
+ ]
156
+ )
157
+ let contextValue = useMemo<WeaverseNextContextValue>(
158
+ () => ({
159
+ merchantOverrides,
160
+ themeSettings: themeSettingsStore.getSnapshot(),
161
+ themeSettingsStore,
162
+ translationStore,
163
+ }),
164
+ [merchantOverrides, themeSettingsStore, translationStore]
165
+ )
166
+
167
+ return (
168
+ <WeaverseNextRootContext.Provider value={rootContextValue}>
169
+ <WeaverseNextContext.Provider value={contextValue}>
170
+ <TranslationProvider
171
+ merchantOverrides={merchantOverrides}
172
+ staticContent={resolvedStaticContent}
173
+ t={t}
174
+ translationStore={translationStore}
175
+ >
176
+ {children}
177
+ </TranslationProvider>
178
+ </WeaverseNextContext.Provider>
179
+ </WeaverseNextRootContext.Provider>
180
+ )
181
+ }