@weaverse/next 0.1.0-alpha.8 → 1.0.0

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 +155 -73
  2. package/dist/{chunk-6PMZ77RA.mjs → chunk-LH6AAEIK.mjs} +115 -8
  3. package/dist/chunk-LH6AAEIK.mjs.map +1 -0
  4. package/dist/index.js +260 -30
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +167 -46
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/server.js +321 -91
  9. package/dist/server.js.map +1 -1
  10. package/dist/server.mjs +205 -28
  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 +60 -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 +21 -0
  29. package/dist/types/renderer.d.ts.map +1 -0
  30. package/dist/types/request-info.d.ts +19 -0
  31. package/dist/types/request-info.d.ts.map +1 -0
  32. package/dist/types/revalidate-item.d.ts +82 -0
  33. package/dist/types/revalidate-item.d.ts.map +1 -0
  34. package/dist/types/root-provider.d.ts +62 -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 +18 -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 +665 -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 +12 -8
  71. package/src/client.ts +102 -0
  72. package/src/hooks.ts +50 -0
  73. package/src/index.ts +153 -0
  74. package/src/item.ts +167 -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 +100 -0
  80. package/src/request-info.ts +124 -0
  81. package/src/revalidate-item.ts +211 -0
  82. package/src/root-provider.tsx +182 -0
  83. package/src/runtime.ts +555 -0
  84. package/src/seo.ts +157 -0
  85. package/src/server/configs.ts +178 -0
  86. package/src/server/normalize-page-url.ts +73 -0
  87. package/src/server/revalidate-handler.ts +357 -0
  88. package/src/server/server-client.ts +728 -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 +128 -0
  94. package/src/theme-settings-store.ts +47 -0
  95. package/src/translation-context.tsx +299 -0
  96. package/src/translation-store.ts +69 -0
  97. package/src/types.ts +753 -0
  98. package/src/use-weaverse-next-studio.tsx +106 -0
  99. package/src/utils.ts +37 -0
  100. package/dist/chunk-6PMZ77RA.mjs.map +0 -1
  101. package/dist/index.d.mts +0 -517
  102. package/dist/index.d.ts +0 -517
  103. package/dist/seo-CpN6tC36.d.mts +0 -477
  104. package/dist/seo-CpN6tC36.d.ts +0 -477
  105. package/dist/server.d.mts +0 -99
  106. package/dist/server.d.ts +0 -99
@@ -0,0 +1,357 @@
1
+ import type { PageType } from '@weaverse/schema'
2
+ import { PageTypeSchema } from '@weaverse/schema'
3
+ import {
4
+ ROUTE_CONTEXT_I18N_KEYS,
5
+ sanitizeRouteContextSearch,
6
+ type WeaverseNextRevalidateRouteContext,
7
+ } from '../revalidate-item'
8
+ import type {
9
+ WeaverseNextComponentData,
10
+ WeaverseNextI18n,
11
+ WeaverseNextRequestContext,
12
+ WeaverseNextServerClient,
13
+ } from '../types'
14
+ import { generateDataFromSchema } from '../utils'
15
+
16
+ const NO_STORE_HEADERS = {
17
+ 'Content-Type': 'application/json',
18
+ 'Cache-Control': 'no-store',
19
+ } as const
20
+
21
+ const STATUS_OK = 200
22
+ const STATUS_BAD_REQUEST = 400
23
+ const STATUS_NOT_FOUND = 404
24
+ const STATUS_UNPROCESSABLE = 422
25
+ const STATUS_SERVER_ERROR = 500
26
+
27
+ const MAX_PATHNAME_LENGTH = 2048
28
+ const MAX_SEARCH_LENGTH = 8192
29
+ const MAX_HANDLE_LENGTH = 512
30
+ const MAX_I18N_FIELD_LENGTH = 256
31
+ // ASCII control characters (C0 range + DEL) are never valid in a route path.
32
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: rejecting control chars in untrusted pathnames is the intent.
33
+ const CONTROL_CHAR_PATTERN = /[\u0000-\u001F\u007F]/
34
+
35
+ /** Configuration for the App Router item-revalidation handler. */
36
+ export interface WeaverseNextRevalidateHandlerConfig {
37
+ /**
38
+ * Reuse the app's server-client factory so the loader runs with the same
39
+ * component registry and commerce (storefront) context as page loads.
40
+ *
41
+ * The optional second argument is the validated, same-origin route context
42
+ * reconstructed from the request body. It is `undefined` for legacy bodies
43
+ * that carry no `routeContext`, so a one-argument `(request) => client`
44
+ * callback remains valid. Consumers must still source project ID, Studio
45
+ * host, API bases/keys, and env from server config — never from this
46
+ * browser-provided context.
47
+ */
48
+ getClient: (
49
+ request: Request,
50
+ requestContext?: WeaverseNextRequestContext
51
+ ) => Promise<WeaverseNextServerClient> | WeaverseNextServerClient
52
+ }
53
+
54
+ /** POST body accepted by the revalidate route. */
55
+ export interface WeaverseNextRevalidateRequestBody {
56
+ /** Unsaved Studio item settings to pass to the registered component loader. */
57
+ draftItem: WeaverseNextComponentData
58
+ /** Public route identity used to reconstruct the loader request context. */
59
+ routeContext?: WeaverseNextRevalidateRouteContext
60
+ }
61
+
62
+ /**
63
+ * Whether a browser-provided pathname is safe to reconstruct a same-origin URL
64
+ * from. Rejects protocol-relative (`//`), absolute-URL, backslash, control
65
+ * character, query-in-path, and fragment-in-path inputs before any URL is
66
+ * built. Literal/encoded dot segments are allowed here and normalized later by
67
+ * the URL pathname setter.
68
+ */
69
+ function isValidPathname(pathname: string): boolean {
70
+ if (!pathname || pathname.length > MAX_PATHNAME_LENGTH) {
71
+ return false
72
+ }
73
+ if (pathname[0] !== '/' || pathname.startsWith('//')) {
74
+ return false
75
+ }
76
+ if (pathname.includes('\\') || pathname.includes('?')) {
77
+ return false
78
+ }
79
+ if (pathname.includes('#') || CONTROL_CHAR_PATTERN.test(pathname)) {
80
+ return false
81
+ }
82
+ return true
83
+ }
84
+
85
+ /**
86
+ * Whether a value is a plain object (literal or `null`-prototype), the only
87
+ * i18n container the wire shape allows. Arrays, class instances, and other
88
+ * exotic objects are rejected so nothing exotic reaches field validation.
89
+ */
90
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
91
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
92
+ return false
93
+ }
94
+ let proto = Object.getPrototypeOf(value)
95
+ return proto === Object.prototype || proto === null
96
+ }
97
+
98
+ /**
99
+ * Validate and pick the allowed i18n fields. Returns `undefined` for a valid
100
+ * empty result and `null` for malformed input so the handler can fail closed.
101
+ *
102
+ * The normative i18n shape carries only the {@link ROUTE_CONTEXT_I18N_KEYS}.
103
+ * Malformed input is rejected rather than silently narrowed: any unknown own
104
+ * key (nested object, extra string, anything), a non-plain container (array,
105
+ * class instance), or a known key whose value is not an optional string of at
106
+ * most {@link MAX_I18N_FIELD_LENGTH} characters fails closed.
107
+ */
108
+ function sanitizeRouteContextI18n(
109
+ input: unknown
110
+ ): WeaverseNextI18n | undefined | null {
111
+ if (input === undefined) {
112
+ return
113
+ }
114
+ if (!isPlainObject(input)) {
115
+ return null
116
+ }
117
+ let source = input
118
+ let allowed = new Set<string>(ROUTE_CONTEXT_I18N_KEYS)
119
+ // Reject unknown own keys instead of dropping them, so a crafted context
120
+ // cannot smuggle extra properties past the trust boundary.
121
+ for (let key of Object.keys(source)) {
122
+ if (!allowed.has(key)) {
123
+ return null
124
+ }
125
+ }
126
+ let result: Record<string, string> = {}
127
+ for (let key of ROUTE_CONTEXT_I18N_KEYS) {
128
+ let value = source[key]
129
+ if (value === undefined) {
130
+ continue
131
+ }
132
+ if (typeof value !== 'string' || value.length > MAX_I18N_FIELD_LENGTH) {
133
+ return null
134
+ }
135
+ result[key] = value
136
+ }
137
+ return Object.keys(result).length ? result : undefined
138
+ }
139
+
140
+ /**
141
+ * Reconstruct a validated, same-origin {@link WeaverseNextRequestContext} from
142
+ * the untrusted route context. Returns `null` for any malformed field so the
143
+ * handler can answer `400 invalid-route-context` before creating a client. The
144
+ * endpoint origin is fixed first, then validated components are assigned, so
145
+ * the browser cannot change protocol, host, port, or credentials.
146
+ */
147
+ function reconstructRouteContext(
148
+ request: Request,
149
+ routeContext: unknown
150
+ ): WeaverseNextRequestContext | null {
151
+ if (
152
+ typeof routeContext !== 'object' ||
153
+ routeContext === null ||
154
+ Array.isArray(routeContext)
155
+ ) {
156
+ return null
157
+ }
158
+ let { pathname, search, handle, pageType, i18n } = routeContext as Record<
159
+ string,
160
+ unknown
161
+ >
162
+
163
+ if (typeof pathname !== 'string' || !isValidPathname(pathname)) {
164
+ return null
165
+ }
166
+
167
+ if (typeof search !== 'string' || search.length > MAX_SEARCH_LENGTH) {
168
+ return null
169
+ }
170
+ let rawSearch = search
171
+ if (rawSearch.length && !rawSearch.startsWith('?')) {
172
+ return null
173
+ }
174
+
175
+ let sanitizedI18n = sanitizeRouteContextI18n(i18n)
176
+ if (sanitizedI18n === null) {
177
+ return null
178
+ }
179
+
180
+ let resolvedPageType: PageType | undefined
181
+ if (pageType !== undefined) {
182
+ let parsed = PageTypeSchema.safeParse(pageType)
183
+ if (!parsed.success) {
184
+ return null
185
+ }
186
+ resolvedPageType = parsed.data
187
+ }
188
+
189
+ let resolvedHandle: string | undefined
190
+ if (handle !== undefined) {
191
+ if (
192
+ typeof handle !== 'string' ||
193
+ !handle ||
194
+ handle.length > MAX_HANDLE_LENGTH
195
+ ) {
196
+ return null
197
+ }
198
+ resolvedHandle = handle
199
+ }
200
+
201
+ // Fix the endpoint origin first, then assign validated components. Reuse the
202
+ // URL's canonical pathname in both context fields so raw input never survives
203
+ // in only one place.
204
+ let endpointUrl = new URL(request.url)
205
+ let routeUrl = new URL(endpointUrl.origin)
206
+ routeUrl.pathname = pathname
207
+ routeUrl.search = sanitizeRouteContextSearch(rawSearch)
208
+ let canonicalPathname = routeUrl.pathname
209
+ let searchParams = routeUrl.searchParams
210
+
211
+ // Last value wins for duplicated Studio controls, matching
212
+ // `getWeaverseNextConfigs()` and its cache-mode resolution.
213
+ let lastParam = (key: string): string | undefined => {
214
+ let values = searchParams.getAll(key)
215
+ return values.length ? values.at(-1) : undefined
216
+ }
217
+
218
+ return {
219
+ pathname: canonicalPathname,
220
+ searchParams,
221
+ url: routeUrl,
222
+ // Headers come from the actual same-origin POST, never its JSON body.
223
+ headers: new Headers(request.headers),
224
+ ...(sanitizedI18n ? { i18n: sanitizedI18n } : {}),
225
+ ...(resolvedPageType ? { pageType: resolvedPageType } : {}),
226
+ ...(resolvedHandle ? { handle: resolvedHandle } : {}),
227
+ isDesignMode: lastParam('isDesignMode') === 'true',
228
+ isPreviewMode: lastParam('isPreviewMode') === 'true',
229
+ isRevisionPreview: searchParams.has('__revisionId'),
230
+ sectionType: lastParam('sectionType') || undefined,
231
+ }
232
+ }
233
+
234
+ function json(body: Record<string, unknown>, status: number) {
235
+ return new Response(JSON.stringify(body), {
236
+ status,
237
+ headers: NO_STORE_HEADERS,
238
+ })
239
+ }
240
+
241
+ function parseDraftItem(payload: unknown): WeaverseNextComponentData | null {
242
+ if (typeof payload !== 'object' || payload === null) {
243
+ return null
244
+ }
245
+ let draftItem = (payload as { draftItem?: unknown }).draftItem
246
+ if (typeof draftItem !== 'object' || draftItem === null) {
247
+ return null
248
+ }
249
+ let { id, type } = draftItem as { id?: unknown; type?: unknown }
250
+ if (typeof id !== 'string' || !id || typeof type !== 'string' || !type) {
251
+ return null
252
+ }
253
+ return draftItem as WeaverseNextComponentData
254
+ }
255
+
256
+ /** App Router handler returned for Studio item revalidation requests. */
257
+ export interface WeaverseNextRevalidateHandler {
258
+ /** Handle a Studio request to rerun one registered component loader. */
259
+ POST: (request: Request) => Promise<Response>
260
+ }
261
+
262
+ /**
263
+ * Create the Studio per-item revalidation route handler.
264
+ *
265
+ * Studio design-mode edits to loader-backed inputs (e.g. resource pickers) need
266
+ * fresh server loader data for a single item. Re-running it through
267
+ * `router.refresh()` re-applies the whole RSC tree — and the draft-item query
268
+ * rewrite changes the App Router page segment key, remounting the page and
269
+ * resetting scroll. This handler runs just the edited component's `loader`
270
+ * with the unsaved draft settings and returns the payload for an in-place
271
+ * client update (see the `2026-07-02--next-item-revalidation` spec).
272
+ *
273
+ * Security: only registered component types are executable, the handler never
274
+ * persists anything, and responses are `no-store`. Exposure is equivalent to
275
+ * the existing `weaverseDraftItem` URL param, which already lets any visitor
276
+ * run design-mode loaders with arbitrary draft settings.
277
+ *
278
+ * @example
279
+ * ```ts
280
+ * // app/api/weaverse/revalidate/route.ts
281
+ * export const { POST } = createWeaverseNextRevalidateHandler({
282
+ * getClient: () => getWeaverseServerClient(Promise.resolve({})),
283
+ * })
284
+ * ```
285
+ */
286
+ export function createWeaverseNextRevalidateHandler(
287
+ config: WeaverseNextRevalidateHandlerConfig
288
+ ): WeaverseNextRevalidateHandler {
289
+ return {
290
+ POST: async (request: Request) => {
291
+ let payload: unknown
292
+ try {
293
+ payload = await request.json()
294
+ } catch {
295
+ return json({ error: 'invalid-payload' }, STATUS_BAD_REQUEST)
296
+ }
297
+
298
+ let draftItem = parseDraftItem(payload)
299
+ if (!draftItem) {
300
+ return json({ error: 'invalid-payload' }, STATUS_BAD_REQUEST)
301
+ }
302
+
303
+ // Missing `routeContext` is valid legacy input; a present but malformed
304
+ // context is rejected at the trust boundary before any client is created.
305
+ let requestContext: WeaverseNextRequestContext | undefined
306
+ let body = payload as { routeContext?: unknown }
307
+ if (Object.hasOwn(body, 'routeContext')) {
308
+ let reconstructed = reconstructRouteContext(request, body.routeContext)
309
+ if (!reconstructed) {
310
+ return json({ error: 'invalid-route-context' }, STATUS_BAD_REQUEST)
311
+ }
312
+ requestContext = reconstructed
313
+ }
314
+
315
+ let client: WeaverseNextServerClient
316
+ try {
317
+ client = await config.getClient(request, requestContext)
318
+ } catch (error) {
319
+ console.error('❌ Revalidate handler getClient failed.', error)
320
+ return json({ error: 'client-init-failed' }, STATUS_SERVER_ERROR)
321
+ }
322
+
323
+ let component = client.components.find(
324
+ (candidate) => candidate?.schema?.type === draftItem.type
325
+ )
326
+ if (!component) {
327
+ return json({ error: 'unknown-component-type' }, STATUS_NOT_FOUND)
328
+ }
329
+ if (typeof component.loader !== 'function') {
330
+ return json({ error: 'missing-loader' }, STATUS_UNPROCESSABLE)
331
+ }
332
+
333
+ try {
334
+ // Same args shape as `runWeaverseComponentLoaders`, so component
335
+ // loaders behave identically between page load and revalidation.
336
+ let loaderData = await component.loader({
337
+ data: {
338
+ ...generateDataFromSchema(component.schema),
339
+ ...draftItem.data,
340
+ },
341
+ weaverse: client,
342
+ context: client.requestContext,
343
+ commerce: client.commerce,
344
+ })
345
+ return json({ loaderData: loaderData ?? null }, STATUS_OK)
346
+ } catch (error) {
347
+ console.error(
348
+ '❌ Item loader revalidation failed.',
349
+ draftItem.type,
350
+ draftItem.id,
351
+ error
352
+ )
353
+ return json({ error: 'loader-failed' }, STATUS_SERVER_ERROR)
354
+ }
355
+ },
356
+ }
357
+ }