@weaverse/next 1.0.0 → 1.0.2
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 +7 -1
- package/dist/client-boundary.js +1061 -0
- package/dist/client-boundary.js.map +1 -0
- package/dist/client-boundary.mjs +1017 -0
- package/dist/client-boundary.mjs.map +1 -0
- package/dist/index.js +307 -1477
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +149 -1100
- package/dist/index.mjs.map +1 -1
- package/dist/seo-BCFNGChj.js +319 -0
- package/dist/seo-BCFNGChj.js.map +1 -0
- package/dist/seo-C-S2viuI.mjs +242 -0
- package/dist/seo-C-S2viuI.mjs.map +1 -0
- package/dist/server.js +626 -1076
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +620 -803
- 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/index.d.ts +11 -12
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/item.d.ts +1 -1
- package/dist/types/item.d.ts.map +1 -1
- package/dist/types/provider.d.ts +1 -1
- package/dist/types/provider.d.ts.map +1 -1
- package/dist/types/revalidate-item.d.ts +1 -1
- package/dist/types/revalidate-item.d.ts.map +1 -1
- package/dist/types/root-provider.d.ts +1 -1
- package/dist/types/root-provider.d.ts.map +1 -1
- package/dist/types/runtime.d.ts.map +1 -1
- package/dist/types/translate.d.ts +56 -0
- package/dist/types/translate.d.ts.map +1 -0
- package/dist/types/translation-context.d.ts +1 -48
- package/dist/types/translation-context.d.ts.map +1 -1
- package/dist/types/translation-store.d.ts.map +1 -1
- package/dist/types/types.d.ts +6 -1
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/utils.d.ts +10 -0
- package/dist/types/utils.d.ts.map +1 -1
- package/package.json +5 -24
- package/src/client-boundary.ts +50 -0
- package/src/index.ts +42 -52
- package/src/provider.tsx +2 -4
- package/src/root-provider.tsx +2 -4
- package/src/translate.ts +125 -0
- package/src/translation-context.tsx +1 -118
- package/src/types.ts +6 -0
- package/src/utils.ts +12 -0
- package/dist/chunk-LH6AAEIK.mjs +0 -326
- package/dist/chunk-LH6AAEIK.mjs.map +0 -1
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
let _weaverse_schema = require("@weaverse/schema");
|
|
2
|
+
//#region src/utils.ts
|
|
3
|
+
function generateDataFromSchema(schema) {
|
|
4
|
+
let data = {};
|
|
5
|
+
if (!schema) return data;
|
|
6
|
+
let groups = schema.settings ?? schema.inspector ?? [];
|
|
7
|
+
for (let group of groups) for (let input of group?.inputs ?? []) {
|
|
8
|
+
let { name, defaultValue } = input;
|
|
9
|
+
if (name && defaultValue !== null && defaultValue !== void 0) data[name] = defaultValue;
|
|
10
|
+
}
|
|
11
|
+
return data;
|
|
12
|
+
}
|
|
13
|
+
let isBrowser = typeof window !== "undefined";
|
|
14
|
+
let isIframe = isBrowser && window.top !== window.self;
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/loader.ts
|
|
17
|
+
function isItemNode(node) {
|
|
18
|
+
return typeof node === "object" && node !== null && typeof node.type === "string";
|
|
19
|
+
}
|
|
20
|
+
function cloneItem(item) {
|
|
21
|
+
return {
|
|
22
|
+
...item,
|
|
23
|
+
children: Array.isArray(item.children) ? item.children.map((child) => isItemNode(child) ? cloneItem(child) : { ...child }) : item.children
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async function runWeaverseComponentLoaders(args) {
|
|
27
|
+
let { client, context, commerce } = args;
|
|
28
|
+
let sourceData = args.data ?? client.data;
|
|
29
|
+
if (!sourceData) return null;
|
|
30
|
+
if (!sourceData.page.items.length) return sourceData;
|
|
31
|
+
let data = {
|
|
32
|
+
...sourceData,
|
|
33
|
+
page: {
|
|
34
|
+
...sourceData.page,
|
|
35
|
+
items: sourceData.page.items.map(cloneItem)
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
let items = data.page.items;
|
|
39
|
+
let componentMap = /* @__PURE__ */ new Map();
|
|
40
|
+
for (let component of client.components) if (component?.schema?.type) componentMap.set(component.schema.type, component);
|
|
41
|
+
let visited = /* @__PURE__ */ new Set();
|
|
42
|
+
let resolvedContext = context ?? client.requestContext;
|
|
43
|
+
let resolvedCommerce = commerce ?? client.commerce;
|
|
44
|
+
let resolvedWeaverse = resolvedCommerce === client.commerce ? client : {
|
|
45
|
+
...client,
|
|
46
|
+
commerce: resolvedCommerce,
|
|
47
|
+
storefront: resolvedCommerce?.storefront
|
|
48
|
+
};
|
|
49
|
+
async function walk(nodes) {
|
|
50
|
+
for (let item of nodes) {
|
|
51
|
+
if (!item || visited.has(item.id)) continue;
|
|
52
|
+
visited.add(item.id);
|
|
53
|
+
let component = componentMap.get(item.type);
|
|
54
|
+
if (component?.loader) try {
|
|
55
|
+
item.loaderData = await component.loader({
|
|
56
|
+
data: {
|
|
57
|
+
...generateDataFromSchema(component.schema),
|
|
58
|
+
...item.data
|
|
59
|
+
},
|
|
60
|
+
weaverse: resolvedWeaverse,
|
|
61
|
+
context: resolvedContext,
|
|
62
|
+
commerce: resolvedCommerce
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.warn("❌ Item loader run failed.", item.type, item.id, error);
|
|
66
|
+
}
|
|
67
|
+
let children = item.children;
|
|
68
|
+
if (Array.isArray(children)) {
|
|
69
|
+
let inlineChildren = children.filter(isItemNode);
|
|
70
|
+
if (inlineChildren.length) await walk(inlineChildren);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
await walk(items);
|
|
75
|
+
return data;
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/request-info.ts
|
|
79
|
+
function toUrl(value) {
|
|
80
|
+
return typeof value === "string" ? new URL(value, "http://localhost") : value;
|
|
81
|
+
}
|
|
82
|
+
function getSearchParamsFromContext(context) {
|
|
83
|
+
if (context?.searchParams) return new URLSearchParams(context.searchParams);
|
|
84
|
+
if (context?.url) {
|
|
85
|
+
let url = toUrl(context.url);
|
|
86
|
+
return new URLSearchParams(url.searchParams);
|
|
87
|
+
}
|
|
88
|
+
return new URLSearchParams();
|
|
89
|
+
}
|
|
90
|
+
function withPathPrefix(pathname, i18n) {
|
|
91
|
+
let segments = (typeof i18n?.pathPrefix === "string" ? i18n.pathPrefix : "").split("/").filter(Boolean);
|
|
92
|
+
let prefix = segments.length > 0 ? `/${segments.join("/")}` : "";
|
|
93
|
+
if (prefix === "" || pathname === prefix || pathname.startsWith(`${prefix}/`)) return pathname;
|
|
94
|
+
return pathname === "/" ? prefix : `${prefix}${pathname}`;
|
|
95
|
+
}
|
|
96
|
+
function getPathnameFromContext(context) {
|
|
97
|
+
if (context?.pathname) return withPathPrefix(context.pathname, context.i18n);
|
|
98
|
+
if (context?.url) return withPathPrefix(toUrl(context.url).pathname || "/", context.i18n);
|
|
99
|
+
return "/";
|
|
100
|
+
}
|
|
101
|
+
function coerceQueryValue(value) {
|
|
102
|
+
if (value === "true") return true;
|
|
103
|
+
if (value === "false") return false;
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
function toQueryRecord(searchParams) {
|
|
107
|
+
let queries = {};
|
|
108
|
+
for (let [key, value] of searchParams.entries()) queries[key] = coerceQueryValue(value);
|
|
109
|
+
return queries;
|
|
110
|
+
}
|
|
111
|
+
const TRANSIENT_STUDIO_PARAMS = new Set(["weaverseDraftItem", "__weaverseDraftItem"]);
|
|
112
|
+
function getRequestInfoSearchParams(context) {
|
|
113
|
+
let searchParams = new URLSearchParams(getSearchParamsFromContext(context));
|
|
114
|
+
for (let key of TRANSIENT_STUDIO_PARAMS) searchParams.delete(key);
|
|
115
|
+
return searchParams;
|
|
116
|
+
}
|
|
117
|
+
function buildWeaverseNextRequestInfo(context) {
|
|
118
|
+
let searchParams = getRequestInfoSearchParams(context);
|
|
119
|
+
let search = searchParams.toString();
|
|
120
|
+
return {
|
|
121
|
+
pathname: getPathnameFromContext(context),
|
|
122
|
+
search: search ? `?${search}` : "",
|
|
123
|
+
queries: toQueryRecord(searchParams),
|
|
124
|
+
...context?.i18n ? { i18n: context.i18n } : {},
|
|
125
|
+
...context?.pageType ? { pageType: context.pageType } : {},
|
|
126
|
+
...context?.handle ? { handle: context.handle } : {}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/revalidate-item.ts
|
|
131
|
+
const DEFAULT_REVALIDATE_ENDPOINT = "/api/weaverse/revalidate";
|
|
132
|
+
const MAX_ROUTE_CONTEXT_HANDLE_LENGTH = 512;
|
|
133
|
+
const DENIED_ROUTE_CONTEXT_PARAMS = [
|
|
134
|
+
"weaverseprojectid",
|
|
135
|
+
"weaversehost",
|
|
136
|
+
"weaverseapikey",
|
|
137
|
+
"weaverseapibase",
|
|
138
|
+
"weaversepublicapibase",
|
|
139
|
+
"weaverseversion",
|
|
140
|
+
"projectid",
|
|
141
|
+
"weaversedraftitem",
|
|
142
|
+
"__weaversedraftitem",
|
|
143
|
+
"_rsc"
|
|
144
|
+
];
|
|
145
|
+
const ROUTE_CONTEXT_I18N_KEYS = [
|
|
146
|
+
"country",
|
|
147
|
+
"label",
|
|
148
|
+
"language",
|
|
149
|
+
"locale",
|
|
150
|
+
"pathPrefix"
|
|
151
|
+
];
|
|
152
|
+
function sanitizeRouteContextSearch(search) {
|
|
153
|
+
let params = new URLSearchParams(search);
|
|
154
|
+
let denied = new Set(DENIED_ROUTE_CONTEXT_PARAMS);
|
|
155
|
+
for (let key of [...new Set(params.keys())]) if (denied.has(key.toLowerCase())) params.delete(key);
|
|
156
|
+
let serialized = params.toString();
|
|
157
|
+
return serialized ? `?${serialized}` : "";
|
|
158
|
+
}
|
|
159
|
+
function buildWeaverseNextRevalidateRouteContext(requestInfo) {
|
|
160
|
+
if (!requestInfo || typeof requestInfo.pathname !== "string" || !requestInfo.pathname) return;
|
|
161
|
+
let routeContext = {
|
|
162
|
+
pathname: requestInfo.pathname,
|
|
163
|
+
search: sanitizeRouteContextSearch(requestInfo.search ?? "")
|
|
164
|
+
};
|
|
165
|
+
let { i18n } = requestInfo;
|
|
166
|
+
if (i18n && typeof i18n === "object") {
|
|
167
|
+
let picked = {};
|
|
168
|
+
for (let key of ROUTE_CONTEXT_I18N_KEYS) {
|
|
169
|
+
let value = i18n[key];
|
|
170
|
+
if (typeof value === "string") picked[key] = value;
|
|
171
|
+
}
|
|
172
|
+
if (Object.keys(picked).length) routeContext.i18n = picked;
|
|
173
|
+
}
|
|
174
|
+
if (requestInfo.pageType && _weaverse_schema.PageTypeSchema.safeParse(requestInfo.pageType).success) routeContext.pageType = requestInfo.pageType;
|
|
175
|
+
if (typeof requestInfo.handle === "string" && requestInfo.handle && requestInfo.handle.length <= MAX_ROUTE_CONTEXT_HANDLE_LENGTH) routeContext.handle = requestInfo.handle;
|
|
176
|
+
return routeContext;
|
|
177
|
+
}
|
|
178
|
+
async function revalidateWeaverseNextItem(runtime, draftItem, endpoint = DEFAULT_REVALIDATE_ENDPOINT) {
|
|
179
|
+
let instance = runtime.itemInstances.get(draftItem.id);
|
|
180
|
+
if (!instance) throw new Error(`[weaverse-next] No live item instance for "${draftItem.id}".`);
|
|
181
|
+
let routeContext = buildWeaverseNextRevalidateRouteContext(runtime.requestInfo);
|
|
182
|
+
let response = await fetch(endpoint, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
headers: { "Content-Type": "application/json" },
|
|
185
|
+
body: JSON.stringify(routeContext ? {
|
|
186
|
+
draftItem,
|
|
187
|
+
routeContext
|
|
188
|
+
} : { draftItem })
|
|
189
|
+
});
|
|
190
|
+
if (!response.ok) throw new Error(`[weaverse-next] Revalidate endpoint responded ${response.status}.`);
|
|
191
|
+
let { loaderData } = await response.json();
|
|
192
|
+
instance.setData({ loaderData: loaderData ?? null });
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/seo.ts
|
|
196
|
+
function hasOpenGraphContent(seo) {
|
|
197
|
+
return Boolean(seo?.title || seo?.description || seo?.image || seo?.type);
|
|
198
|
+
}
|
|
199
|
+
function hasTwitterContent(seo) {
|
|
200
|
+
return Boolean(seo?.title || seo?.description || seo?.image || seo?.cardType);
|
|
201
|
+
}
|
|
202
|
+
function normalizeTwitterCard(cardType) {
|
|
203
|
+
return cardType === "summary_large_image" ? "summary_large_image" : "summary";
|
|
204
|
+
}
|
|
205
|
+
function normalizeOpenGraphType(type) {
|
|
206
|
+
if (type === "article" || type === "profile" || type === "video.other") return type;
|
|
207
|
+
return "website";
|
|
208
|
+
}
|
|
209
|
+
function formatWeaverseNextSeoMetadata(seo) {
|
|
210
|
+
let metadata = { robots: {
|
|
211
|
+
index: seo?.robots?.index !== false,
|
|
212
|
+
follow: seo?.robots?.follow !== false
|
|
213
|
+
} };
|
|
214
|
+
if (!seo) return metadata;
|
|
215
|
+
if (seo.title) metadata.title = seo.title;
|
|
216
|
+
if (seo.description) metadata.description = seo.description;
|
|
217
|
+
if (seo.keywords) metadata.keywords = seo.keywords;
|
|
218
|
+
if (seo.canonicalUrl) metadata.alternates = { canonical: seo.canonicalUrl };
|
|
219
|
+
let openGraph = seo.openGraph;
|
|
220
|
+
if (hasOpenGraphContent(openGraph)) metadata.openGraph = {
|
|
221
|
+
title: openGraph?.title,
|
|
222
|
+
description: openGraph?.description,
|
|
223
|
+
images: openGraph?.image ? [openGraph.image] : void 0,
|
|
224
|
+
type: normalizeOpenGraphType(openGraph?.type)
|
|
225
|
+
};
|
|
226
|
+
let twitter = seo.twitter;
|
|
227
|
+
if (hasTwitterContent(twitter)) metadata.twitter = {
|
|
228
|
+
card: normalizeTwitterCard(twitter?.cardType),
|
|
229
|
+
title: twitter?.title,
|
|
230
|
+
description: twitter?.description,
|
|
231
|
+
images: twitter?.image ? [twitter.image] : void 0
|
|
232
|
+
};
|
|
233
|
+
return metadata;
|
|
234
|
+
}
|
|
235
|
+
function getWeaverseNextSeoMetadata(data) {
|
|
236
|
+
if (!data) return formatWeaverseNextSeoMetadata(null);
|
|
237
|
+
return formatWeaverseNextSeoMetadata(("items" in data && "id" in data ? data : data.page ?? null)?.seo ?? null);
|
|
238
|
+
}
|
|
239
|
+
//#endregion
|
|
240
|
+
Object.defineProperty(exports, "DEFAULT_REVALIDATE_ENDPOINT", {
|
|
241
|
+
enumerable: true,
|
|
242
|
+
get: function() {
|
|
243
|
+
return DEFAULT_REVALIDATE_ENDPOINT;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
Object.defineProperty(exports, "ROUTE_CONTEXT_I18N_KEYS", {
|
|
247
|
+
enumerable: true,
|
|
248
|
+
get: function() {
|
|
249
|
+
return ROUTE_CONTEXT_I18N_KEYS;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
Object.defineProperty(exports, "buildWeaverseNextRequestInfo", {
|
|
253
|
+
enumerable: true,
|
|
254
|
+
get: function() {
|
|
255
|
+
return buildWeaverseNextRequestInfo;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
Object.defineProperty(exports, "buildWeaverseNextRevalidateRouteContext", {
|
|
259
|
+
enumerable: true,
|
|
260
|
+
get: function() {
|
|
261
|
+
return buildWeaverseNextRevalidateRouteContext;
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
Object.defineProperty(exports, "formatWeaverseNextSeoMetadata", {
|
|
265
|
+
enumerable: true,
|
|
266
|
+
get: function() {
|
|
267
|
+
return formatWeaverseNextSeoMetadata;
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
Object.defineProperty(exports, "generateDataFromSchema", {
|
|
271
|
+
enumerable: true,
|
|
272
|
+
get: function() {
|
|
273
|
+
return generateDataFromSchema;
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
Object.defineProperty(exports, "getWeaverseNextSeoMetadata", {
|
|
277
|
+
enumerable: true,
|
|
278
|
+
get: function() {
|
|
279
|
+
return getWeaverseNextSeoMetadata;
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
Object.defineProperty(exports, "isBrowser", {
|
|
283
|
+
enumerable: true,
|
|
284
|
+
get: function() {
|
|
285
|
+
return isBrowser;
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
Object.defineProperty(exports, "isIframe", {
|
|
289
|
+
enumerable: true,
|
|
290
|
+
get: function() {
|
|
291
|
+
return isIframe;
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
Object.defineProperty(exports, "revalidateWeaverseNextItem", {
|
|
295
|
+
enumerable: true,
|
|
296
|
+
get: function() {
|
|
297
|
+
return revalidateWeaverseNextItem;
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
Object.defineProperty(exports, "runWeaverseComponentLoaders", {
|
|
301
|
+
enumerable: true,
|
|
302
|
+
get: function() {
|
|
303
|
+
return runWeaverseComponentLoaders;
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
Object.defineProperty(exports, "sanitizeRouteContextSearch", {
|
|
307
|
+
enumerable: true,
|
|
308
|
+
get: function() {
|
|
309
|
+
return sanitizeRouteContextSearch;
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
Object.defineProperty(exports, "withPathPrefix", {
|
|
313
|
+
enumerable: true,
|
|
314
|
+
get: function() {
|
|
315
|
+
return withPathPrefix;
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
//# sourceMappingURL=seo-BCFNGChj.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seo-BCFNGChj.js","names":["PageTypeSchema"],"sources":["../src/utils.ts","../src/loader.ts","../src/request-info.ts","../src/revalidate-item.ts","../src/seo.ts"],"sourcesContent":["import type { SchemaType } from '@weaverse/schema'\nimport type { WeaverseNextThemeSchema } from './types'\n\n/**\n * Generate default component data from a schema's `settings` inputs.\n *\n * Mirrors Hydrogen's `generateDataFromSchema` (settings → `{ [name]:\n * defaultValue }`) but is kept local so `@weaverse/next` doesn't depend on\n * `@weaverse/hydrogen`. Falls back to the deprecated `inspector` array when a\n * schema predates `settings`.\n */\nexport function generateDataFromSchema(\n schema: SchemaType | WeaverseNextThemeSchema | undefined\n): Record<string, unknown> {\n let data: Record<string, unknown> = {}\n if (!schema) {\n return data\n }\n\n let groups =\n (schema.settings as {\n inputs?: { name?: string; defaultValue?: unknown }[]\n }[]) ??\n (schema as { inspector?: typeof groups }).inspector ??\n []\n\n for (let group of groups) {\n for (let input of group?.inputs ?? []) {\n let { name, defaultValue } = input\n if (name && defaultValue !== null && defaultValue !== undefined) {\n data[name] = defaultValue\n }\n }\n }\n\n return data\n}\n\n/**\n * Whether the code runs in a browser window.\n *\n * Defined here rather than re-exported from `@weaverse/react`, which sits\n * behind the `'use client'` boundary: there a Server Component would receive a\n * client reference, a truthy object, instead of `false`.\n */\nexport let isBrowser = typeof window !== 'undefined'\n\n/** Whether the code runs in a browser window embedded in an iframe. */\nexport let isIframe = isBrowser && window.top !== window.self\n","import type {\n WeaverseNextClient,\n WeaverseNextCommerceContext,\n WeaverseNextComponent,\n WeaverseNextComponentData,\n WeaverseNextLoaderData,\n WeaverseNextRequestContext,\n} from './types'\nimport { generateDataFromSchema } from './utils'\n\n/** Inputs for running registered component loaders over a page tree. */\nexport interface RunComponentLoadersArgs {\n /** Weaverse client containing the component registry. */\n client: WeaverseNextClient\n /** Request-scoped commerce override; defaults to `client.commerce`. */\n commerce?: WeaverseNextCommerceContext\n /** Request-context override; defaults to `client.requestContext`. */\n context?: WeaverseNextRequestContext\n /** Page payload to walk; defaults to `client.data`. */\n data?: WeaverseNextLoaderData | null\n}\n\nfunction isItemNode(node: unknown): node is WeaverseNextComponentData {\n return (\n typeof node === 'object' &&\n node !== null &&\n typeof (node as WeaverseNextComponentData).type === 'string'\n )\n}\n\nfunction cloneItem(item: WeaverseNextComponentData): WeaverseNextComponentData {\n return {\n ...item,\n children: Array.isArray(item.children)\n ? item.children.map((child) =>\n isItemNode(child) ? cloneItem(child) : { ...child }\n )\n : item.children,\n }\n}\n\n/**\n * Walk a serialized page tree and run each registered component's `loader`,\n * attaching the result to the item as `loaderData`.\n *\n * Loaders receive explicit `commerce.storefront` (and `weaverse.storefront` as\n * a compatibility alias via the client). Children are walked recursively when\n * they are inline item objects; flat `items` arrays are covered by the\n * top-level walk.\n *\n * @returns a cloned `data` object with `loaderData` attached to cloned items.\n */\nexport async function runWeaverseComponentLoaders(\n args: RunComponentLoadersArgs\n): Promise<WeaverseNextLoaderData | null> {\n let { client, context, commerce } = args\n let sourceData = args.data ?? client.data\n if (!sourceData) {\n return null\n }\n if (!sourceData.page.items.length) {\n return sourceData\n }\n\n let data: WeaverseNextLoaderData = {\n ...sourceData,\n page: {\n ...sourceData.page,\n items: sourceData.page.items.map(cloneItem),\n },\n }\n let items = data.page.items\n\n let componentMap = new Map<string, WeaverseNextComponent>()\n for (let component of client.components) {\n if (component?.schema?.type) {\n componentMap.set(component.schema.type, component)\n }\n }\n\n let visited = new Set<string>()\n let resolvedContext = context ?? client.requestContext\n let resolvedCommerce = commerce ?? client.commerce\n\n // Per-call `weaverse` alias. When a request-scoped `commerce` is passed, the\n // `weaverse.storefront` compatibility alias must resolve to *that* storefront,\n // not the client's construction-time commerce. Pilot-style loaders call\n // `weaverse.storefront.query(...)`, so a stale alias would query the wrong\n // (or an undefined) storefront. When no override is given we reuse the client\n // as-is to preserve its prototype getters.\n let resolvedWeaverse: WeaverseNextClient =\n resolvedCommerce === client.commerce\n ? client\n : {\n ...client,\n commerce: resolvedCommerce,\n storefront: resolvedCommerce?.storefront,\n }\n\n async function walk(nodes: WeaverseNextComponentData[]) {\n for (let item of nodes) {\n if (!item || visited.has(item.id)) {\n continue\n }\n visited.add(item.id)\n\n let component = componentMap.get(item.type)\n if (component?.loader) {\n try {\n item.loaderData = await component.loader({\n data: { ...generateDataFromSchema(component.schema), ...item.data },\n weaverse: resolvedWeaverse,\n context: resolvedContext,\n commerce: resolvedCommerce,\n })\n } catch (error) {\n console.warn('❌ Item loader run failed.', item.type, item.id, error)\n }\n }\n\n let children = item.children\n if (Array.isArray(children)) {\n let inlineChildren = children.filter(\n isItemNode\n ) as WeaverseNextComponentData[]\n if (inlineChildren.length) {\n await walk(inlineChildren)\n }\n }\n }\n }\n\n await walk(items)\n return data\n}\n","import type {\n WeaverseNextRequestContext,\n WeaverseNextRequestInfo,\n} from './types'\n\nfunction toUrl(value: string | URL): URL {\n return typeof value === 'string' ? new URL(value, 'http://localhost') : value\n}\n\nfunction getSearchParamsFromContext(\n context?: WeaverseNextRequestContext\n): URLSearchParams {\n if (context?.searchParams) {\n return new URLSearchParams(context.searchParams)\n }\n\n if (context?.url) {\n let url = toUrl(context.url)\n return new URLSearchParams(url.searchParams)\n }\n\n return new URLSearchParams()\n}\n\n/**\n * Put the request's market prefix (`i18n.pathPrefix`, e.g. `/de-de`) on a\n * pathname that does not carry it yet.\n *\n * A Next app builds its request context from the route path, and with\n * locale-segmented routes that path is usually unprefixed. Studio's address\n * bar follows `requestInfo.pathname`, so without the prefix it snaps back to\n * the default market after every navigation. A path that already carries the\n * prefix, and the default market's empty prefix, pass through unchanged.\n */\nexport function withPathPrefix(\n pathname: string,\n i18n?: WeaverseNextRequestContext['i18n']\n): string {\n let raw = typeof i18n?.pathPrefix === 'string' ? i18n.pathPrefix : ''\n // Split rather than regex-trim: `pathPrefix` is caller input, and an anchored\n // `/\\/+$/` backtracks polynomially on long runs of slashes.\n let segments = raw.split('/').filter(Boolean)\n let prefix = segments.length > 0 ? `/${segments.join('/')}` : ''\n if (\n prefix === '' ||\n pathname === prefix ||\n pathname.startsWith(`${prefix}/`)\n ) {\n return pathname\n }\n return pathname === '/' ? prefix : `${prefix}${pathname}`\n}\n\nfunction getPathnameFromContext(context?: WeaverseNextRequestContext): string {\n if (context?.pathname) {\n return withPathPrefix(context.pathname, context.i18n)\n }\n\n if (context?.url) {\n let url = toUrl(context.url)\n return withPathPrefix(url.pathname || '/', context.i18n)\n }\n\n return '/'\n}\n\nfunction coerceQueryValue(value: string): string | boolean {\n if (value === 'true') {\n return true\n }\n if (value === 'false') {\n return false\n }\n return value\n}\n\nfunction toQueryRecord(\n searchParams: URLSearchParams\n): Record<string, string | boolean> {\n let queries: Record<string, string | boolean> = {}\n for (let [key, value] of searchParams.entries()) {\n queries[key] = coerceQueryValue(value)\n }\n return queries\n}\n\nconst TRANSIENT_STUDIO_PARAMS = new Set([\n 'weaverseDraftItem',\n // Legacy name from the original design-mode draft-item spike. Keep it out of\n // runtime requestInfo too, in case an older Builder bundle is in front of a\n // newer Next storefront.\n '__weaverseDraftItem',\n])\n\nfunction getRequestInfoSearchParams(\n context?: WeaverseNextRequestContext\n): URLSearchParams {\n let searchParams = new URLSearchParams(getSearchParamsFromContext(context))\n for (let key of TRANSIENT_STUDIO_PARAMS) {\n searchParams.delete(key)\n }\n return searchParams\n}\n\n/**\n * Build the Hydrogen-like request info shape consumed by the Studio bridge.\n * Duplicate query keys keep the last value, matching `URLSearchParams` iteration\n * assignment and Hydrogen's practical query-object behavior.\n */\nexport function buildWeaverseNextRequestInfo(\n context?: WeaverseNextRequestContext\n): WeaverseNextRequestInfo {\n let searchParams = getRequestInfoSearchParams(context)\n let search = searchParams.toString()\n\n return {\n pathname: getPathnameFromContext(context),\n search: search ? `?${search}` : '',\n queries: toQueryRecord(searchParams),\n ...(context?.i18n ? { i18n: context.i18n } : {}),\n ...(context?.pageType ? { pageType: context.pageType } : {}),\n ...(context?.handle ? { handle: context.handle } : {}),\n }\n}\n","import type { PageType } from '@weaverse/schema'\nimport { PageTypeSchema } from '@weaverse/schema'\nimport type {\n WeaverseNextComponentData,\n WeaverseNextI18n,\n WeaverseNextRequestInfo,\n} from './types'\n\n/** Default App Router endpoint used for Studio item-loader revalidation. */\nexport const DEFAULT_REVALIDATE_ENDPOINT = '/api/weaverse/revalidate'\n\nconst MAX_ROUTE_CONTEXT_HANDLE_LENGTH = 512\n\n/**\n * Server-owned and transient controls stripped from the revalidation route\n * context on both sides of the trust boundary. Compared case-insensitively, so\n * every entry is stored lowercased. Client sanitization keeps normal traffic\n * clean; the route handler repeats this deny-list as the security boundary.\n *\n * - Server-owned: a crafted `?weaverseProjectId=`/`?weaverseHost=` must never\n * influence server config resolution (see `getWeaverseNextConfigs`), and\n * credential-shaped params never ride along in route context.\n * - Transient: draft-item and RSC transport params never describe route identity.\n */\nexport const DENIED_ROUTE_CONTEXT_PARAMS = [\n 'weaverseprojectid',\n 'weaversehost',\n 'weaverseapikey',\n 'weaverseapibase',\n 'weaversepublicapibase',\n 'weaverseversion',\n 'projectid',\n 'weaversedraftitem',\n '__weaversedraftitem',\n '_rsc',\n]\n\n/** i18n fields allowed to cross the browser boundary. */\nexport const ROUTE_CONTEXT_I18N_KEYS = [\n 'country',\n 'label',\n 'language',\n 'locale',\n 'pathPrefix',\n] as const\n\n/**\n * JSON-safe route subset that may cross the browser boundary alongside the\n * draft item. Never carries headers, cookies, auth, env, project ID, Studio\n * host, API keys/bases, commerce clients, the request context, runtime, or\n * client. `search` is canonical (preserves duplicate keys); the derived\n * `queries` record is intentionally omitted.\n */\nexport interface WeaverseNextRevalidateRouteContext {\n /** Route handle used for page assignment. */\n handle?: string\n /** Public locale and market identity allowed across the browser boundary. */\n i18n?: Pick<\n WeaverseNextI18n,\n 'country' | 'label' | 'language' | 'locale' | 'pathPrefix'\n >\n /** Weaverse page type resolved by the active route. */\n pageType?: PageType\n /** Canonical same-origin route pathname. */\n pathname: string\n /** Sanitized serialized query string, including the leading `?` when set. */\n search: string\n}\n\n/**\n * Minimal runtime surface the in-place apply needs. Structural (not the\n * concrete `WeaverseNextRuntime` class) so unit tests can drive it with a\n * plain mock. `requestInfo` stays optional: a real `WeaverseNextRuntime`\n * always supplies it, but when absent the client omits `routeContext` and the\n * legacy `{ draftItem }` request is preserved.\n */\nexport interface RevalidateItemRuntimeLike {\n /** Live item stores keyed by Weaverse item ID. */\n itemInstances: Map<\n string,\n {\n /** Merge refreshed loader data into the live item store. */\n setData: (update: Record<string, unknown>) => unknown\n }\n >\n /** Active route identity used to construct the optional revalidation payload. */\n requestInfo?: WeaverseNextRequestInfo\n}\n\n/**\n * Strip denied (server-owned/transient) controls from a `search` string and\n * re-serialize it. Preserves ordinary application queries, duplicate keys,\n * encoded Unicode, and mode controls (`isDesignMode`, `isPreviewMode`,\n * `__revisionId`, `sectionType`). Returns `''` or a `?`-prefixed string.\n */\nexport function sanitizeRouteContextSearch(search: string): string {\n let params = new URLSearchParams(search)\n let denied = new Set(DENIED_ROUTE_CONTEXT_PARAMS)\n for (let key of [...new Set(params.keys())]) {\n if (denied.has(key.toLowerCase())) {\n params.delete(key)\n }\n }\n let serialized = params.toString()\n return serialized ? `?${serialized}` : ''\n}\n\n/**\n * Build the sanitized {@link WeaverseNextRevalidateRouteContext} from the\n * active runtime's request info. Returns `undefined` without a usable pathname\n * so the caller omits `routeContext` and keeps the legacy request. This is not\n * the security boundary — the route handler re-validates and re-sanitizes.\n */\nexport function buildWeaverseNextRevalidateRouteContext(\n requestInfo?: WeaverseNextRequestInfo\n): WeaverseNextRevalidateRouteContext | undefined {\n if (\n !requestInfo ||\n typeof requestInfo.pathname !== 'string' ||\n !requestInfo.pathname\n ) {\n return\n }\n\n let routeContext: WeaverseNextRevalidateRouteContext = {\n pathname: requestInfo.pathname,\n search: sanitizeRouteContextSearch(requestInfo.search ?? ''),\n }\n\n let { i18n } = requestInfo\n if (i18n && typeof i18n === 'object') {\n let picked: Record<string, string> = {}\n for (let key of ROUTE_CONTEXT_I18N_KEYS) {\n let value = i18n[key]\n if (typeof value === 'string') {\n picked[key] = value\n }\n }\n if (Object.keys(picked).length) {\n routeContext.i18n = picked\n }\n }\n\n if (\n requestInfo.pageType &&\n PageTypeSchema.safeParse(requestInfo.pageType).success\n ) {\n routeContext.pageType = requestInfo.pageType\n }\n\n if (\n typeof requestInfo.handle === 'string' &&\n requestInfo.handle &&\n requestInfo.handle.length <= MAX_ROUTE_CONTEXT_HANDLE_LENGTH\n ) {\n routeContext.handle = requestInfo.handle\n }\n\n return routeContext\n}\n\n/**\n * Studio per-item loader revalidation: POST the unsaved draft item (and, when\n * the runtime exposes route context, a sanitized `routeContext`) to the app's\n * revalidate route (see `createWeaverseNextRevalidateHandler`) and apply the\n * returned `loaderData` to the live item instance in place.\n *\n * No `router.refresh()` is involved, so the RSC tree is never re-applied — no\n * page remount, no scroll reset, and the draft payload never touches the URL.\n *\n * Throws on any failure (missing route, non-OK response, unknown item) so the\n * Builder Studio bridge can detect the miss and fall back to its legacy\n * refresh-based revalidation for this window.\n */\nexport async function revalidateWeaverseNextItem(\n runtime: RevalidateItemRuntimeLike,\n draftItem: WeaverseNextComponentData,\n endpoint: string = DEFAULT_REVALIDATE_ENDPOINT\n): Promise<void> {\n let instance = runtime.itemInstances.get(draftItem.id)\n if (!instance) {\n throw new Error(\n `[weaverse-next] No live item instance for \"${draftItem.id}\".`\n )\n }\n\n // Snapshot route identity at revalidation start so a later navigation cannot\n // retarget this in-flight request. Omit `routeContext` for legacy runtimes.\n let routeContext = buildWeaverseNextRevalidateRouteContext(\n runtime.requestInfo\n )\n let body = routeContext ? { draftItem, routeContext } : { draftItem }\n\n let response = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n })\n if (!response.ok) {\n throw new Error(\n `[weaverse-next] Revalidate endpoint responded ${response.status}.`\n )\n }\n\n let { loaderData } = (await response.json()) as { loaderData?: unknown }\n // The item's settings were already mutated by the Studio edit; only the\n // loader payload needs applying. `setData` merges and notifies subscribers,\n // and the new `loaderData` identity is what Builder's revalidation poll\n // observes for completion.\n instance.setData({ loaderData: loaderData ?? null })\n}\n","import type { PageSEOData } from '@weaverse/schema'\nimport type { Metadata } from 'next'\nimport type { WeaverseNextLoaderData, WeaverseNextPageData } from './types'\n\n/** Object containing an optional Weaverse page for SEO extraction. */\nexport interface WeaverseNextSeoPageContainer {\n /** Page whose published SEO fields should be formatted. */\n page?: WeaverseNextPageData | null\n}\n\n/** Input accepted by {@link getWeaverseNextSeoMetadata}. */\nexport type WeaverseNextSeoInput =\n | WeaverseNextLoaderData\n | WeaverseNextPageData\n | WeaverseNextSeoPageContainer\n | null\n | undefined\n\n/** Open Graph metadata accepted by Next.js's `Metadata` API. */\nexport type NextOpenGraph = NonNullable<Metadata['openGraph']>\n\n/** Twitter card metadata accepted by Next.js's `Metadata` API. */\nexport type NextTwitter = NonNullable<Metadata['twitter']>\n\n/**\n * Weaverse page SEO expressed as a subset of Next's `Metadata`. Assignable to\n * `Metadata` as-is: `const metadata: Metadata = getWeaverseNextSeoMetadata(data)`.\n */\nexport interface WeaverseNextSeoMetadata {\n /** Canonical URL settings for Next's metadata API. */\n alternates?: {\n /** Preferred URL for search indexing. */\n canonical?: string\n }\n /** Page meta description. */\n description?: string\n /** Page meta keywords. */\n keywords?: string\n /** Open Graph metadata normalized to Next's supported union. */\n openGraph?: NextOpenGraph\n /** Search crawler indexing and link-following directives. */\n robots: {\n /** Whether crawlers may follow links on the page. */\n follow: boolean\n /** Whether crawlers may index the page. */\n index: boolean\n }\n /** Page title. */\n title?: string\n /** Twitter card metadata normalized for Next's metadata API. */\n twitter?: NextTwitter\n}\n\nfunction hasOpenGraphContent(seo: PageSEOData['openGraph']): boolean {\n return Boolean(seo?.title || seo?.description || seo?.image || seo?.type)\n}\n\nfunction hasTwitterContent(seo: PageSEOData['twitter']): boolean {\n return Boolean(seo?.title || seo?.description || seo?.image || seo?.cardType)\n}\n\nfunction normalizeTwitterCard(\n cardType: NonNullable<PageSEOData['twitter']>['cardType']\n): 'summary' | 'summary_large_image' {\n return cardType === 'summary_large_image' ? 'summary_large_image' : 'summary'\n}\n\n/**\n * Map a Builder Open Graph type onto Next's `OpenGraphType` union. Every\n * Builder value except `product` exists in Next's union and is preserved\n * verbatim; `product` is not modeled by Next and makes `generateMetadata()`\n * throw, so it degrades to `website` — the closest renderable equivalent.\n */\nfunction normalizeOpenGraphType(\n type: NonNullable<PageSEOData['openGraph']>['type']\n): 'article' | 'profile' | 'video.other' | 'website' {\n if (type === 'article' || type === 'profile' || type === 'video.other') {\n return type\n }\n return 'website'\n}\n\n/**\n * Convert Builder SEO fields into metadata accepted by Next.js.\n *\n * Unsupported Open Graph product types and incomplete Twitter app/player\n * cards are safely downgraded to renderable Next.js variants.\n */\nexport function formatWeaverseNextSeoMetadata(\n seo: PageSEOData | null | undefined\n): WeaverseNextSeoMetadata {\n let index = seo?.robots?.index !== false\n let follow = seo?.robots?.follow !== false\n let metadata: WeaverseNextSeoMetadata = {\n robots: { index, follow },\n }\n\n if (!seo) {\n return metadata\n }\n\n if (seo.title) {\n metadata.title = seo.title\n }\n if (seo.description) {\n metadata.description = seo.description\n }\n if (seo.keywords) {\n metadata.keywords = seo.keywords\n }\n if (seo.canonicalUrl) {\n metadata.alternates = { canonical: seo.canonicalUrl }\n }\n\n let openGraph = seo.openGraph\n if (hasOpenGraphContent(openGraph)) {\n metadata.openGraph = {\n title: openGraph?.title,\n description: openGraph?.description,\n images: openGraph?.image ? [openGraph.image] : undefined,\n type: normalizeOpenGraphType(openGraph?.type),\n }\n }\n\n let twitter = seo.twitter\n if (hasTwitterContent(twitter)) {\n // Builder does not collect the descriptors Next requires for `app` cards\n // (Next throws when they are missing) or `player` cards (which render an\n // empty, useless card without a player URL and dimensions). Downgrade both\n // to `summary` rather than emitting metadata Next cannot render.\n metadata.twitter = {\n card: normalizeTwitterCard(twitter?.cardType),\n title: twitter?.title,\n description: twitter?.description,\n images: twitter?.image ? [twitter.image] : undefined,\n }\n }\n\n return metadata\n}\n\n/**\n * Read page SEO from loader data or a page payload and format it for Next.js\n * `generateMetadata`. Missing data returns indexable/followable robot defaults.\n */\nexport function getWeaverseNextSeoMetadata(\n data: WeaverseNextSeoInput\n): WeaverseNextSeoMetadata {\n if (!data) {\n return formatWeaverseNextSeoMetadata(null)\n }\n let page =\n 'items' in data && 'id' in data\n ? (data as WeaverseNextPageData)\n : ((data as WeaverseNextSeoPageContainer).page ?? null)\n return formatWeaverseNextSeoMetadata(page?.seo ?? null)\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { PageTypeSchema } from "@weaverse/schema";
|
|
2
|
+
//#region src/utils.ts
|
|
3
|
+
function generateDataFromSchema(schema) {
|
|
4
|
+
let data = {};
|
|
5
|
+
if (!schema) return data;
|
|
6
|
+
let groups = schema.settings ?? schema.inspector ?? [];
|
|
7
|
+
for (let group of groups) for (let input of group?.inputs ?? []) {
|
|
8
|
+
let { name, defaultValue } = input;
|
|
9
|
+
if (name && defaultValue !== null && defaultValue !== void 0) data[name] = defaultValue;
|
|
10
|
+
}
|
|
11
|
+
return data;
|
|
12
|
+
}
|
|
13
|
+
let isBrowser = typeof window !== "undefined";
|
|
14
|
+
let isIframe = isBrowser && window.top !== window.self;
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/loader.ts
|
|
17
|
+
function isItemNode(node) {
|
|
18
|
+
return typeof node === "object" && node !== null && typeof node.type === "string";
|
|
19
|
+
}
|
|
20
|
+
function cloneItem(item) {
|
|
21
|
+
return {
|
|
22
|
+
...item,
|
|
23
|
+
children: Array.isArray(item.children) ? item.children.map((child) => isItemNode(child) ? cloneItem(child) : { ...child }) : item.children
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async function runWeaverseComponentLoaders(args) {
|
|
27
|
+
let { client, context, commerce } = args;
|
|
28
|
+
let sourceData = args.data ?? client.data;
|
|
29
|
+
if (!sourceData) return null;
|
|
30
|
+
if (!sourceData.page.items.length) return sourceData;
|
|
31
|
+
let data = {
|
|
32
|
+
...sourceData,
|
|
33
|
+
page: {
|
|
34
|
+
...sourceData.page,
|
|
35
|
+
items: sourceData.page.items.map(cloneItem)
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
let items = data.page.items;
|
|
39
|
+
let componentMap = /* @__PURE__ */ new Map();
|
|
40
|
+
for (let component of client.components) if (component?.schema?.type) componentMap.set(component.schema.type, component);
|
|
41
|
+
let visited = /* @__PURE__ */ new Set();
|
|
42
|
+
let resolvedContext = context ?? client.requestContext;
|
|
43
|
+
let resolvedCommerce = commerce ?? client.commerce;
|
|
44
|
+
let resolvedWeaverse = resolvedCommerce === client.commerce ? client : {
|
|
45
|
+
...client,
|
|
46
|
+
commerce: resolvedCommerce,
|
|
47
|
+
storefront: resolvedCommerce?.storefront
|
|
48
|
+
};
|
|
49
|
+
async function walk(nodes) {
|
|
50
|
+
for (let item of nodes) {
|
|
51
|
+
if (!item || visited.has(item.id)) continue;
|
|
52
|
+
visited.add(item.id);
|
|
53
|
+
let component = componentMap.get(item.type);
|
|
54
|
+
if (component?.loader) try {
|
|
55
|
+
item.loaderData = await component.loader({
|
|
56
|
+
data: {
|
|
57
|
+
...generateDataFromSchema(component.schema),
|
|
58
|
+
...item.data
|
|
59
|
+
},
|
|
60
|
+
weaverse: resolvedWeaverse,
|
|
61
|
+
context: resolvedContext,
|
|
62
|
+
commerce: resolvedCommerce
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.warn("❌ Item loader run failed.", item.type, item.id, error);
|
|
66
|
+
}
|
|
67
|
+
let children = item.children;
|
|
68
|
+
if (Array.isArray(children)) {
|
|
69
|
+
let inlineChildren = children.filter(isItemNode);
|
|
70
|
+
if (inlineChildren.length) await walk(inlineChildren);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
await walk(items);
|
|
75
|
+
return data;
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/request-info.ts
|
|
79
|
+
function toUrl(value) {
|
|
80
|
+
return typeof value === "string" ? new URL(value, "http://localhost") : value;
|
|
81
|
+
}
|
|
82
|
+
function getSearchParamsFromContext(context) {
|
|
83
|
+
if (context?.searchParams) return new URLSearchParams(context.searchParams);
|
|
84
|
+
if (context?.url) {
|
|
85
|
+
let url = toUrl(context.url);
|
|
86
|
+
return new URLSearchParams(url.searchParams);
|
|
87
|
+
}
|
|
88
|
+
return new URLSearchParams();
|
|
89
|
+
}
|
|
90
|
+
function withPathPrefix(pathname, i18n) {
|
|
91
|
+
let segments = (typeof i18n?.pathPrefix === "string" ? i18n.pathPrefix : "").split("/").filter(Boolean);
|
|
92
|
+
let prefix = segments.length > 0 ? `/${segments.join("/")}` : "";
|
|
93
|
+
if (prefix === "" || pathname === prefix || pathname.startsWith(`${prefix}/`)) return pathname;
|
|
94
|
+
return pathname === "/" ? prefix : `${prefix}${pathname}`;
|
|
95
|
+
}
|
|
96
|
+
function getPathnameFromContext(context) {
|
|
97
|
+
if (context?.pathname) return withPathPrefix(context.pathname, context.i18n);
|
|
98
|
+
if (context?.url) return withPathPrefix(toUrl(context.url).pathname || "/", context.i18n);
|
|
99
|
+
return "/";
|
|
100
|
+
}
|
|
101
|
+
function coerceQueryValue(value) {
|
|
102
|
+
if (value === "true") return true;
|
|
103
|
+
if (value === "false") return false;
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
function toQueryRecord(searchParams) {
|
|
107
|
+
let queries = {};
|
|
108
|
+
for (let [key, value] of searchParams.entries()) queries[key] = coerceQueryValue(value);
|
|
109
|
+
return queries;
|
|
110
|
+
}
|
|
111
|
+
const TRANSIENT_STUDIO_PARAMS = new Set(["weaverseDraftItem", "__weaverseDraftItem"]);
|
|
112
|
+
function getRequestInfoSearchParams(context) {
|
|
113
|
+
let searchParams = new URLSearchParams(getSearchParamsFromContext(context));
|
|
114
|
+
for (let key of TRANSIENT_STUDIO_PARAMS) searchParams.delete(key);
|
|
115
|
+
return searchParams;
|
|
116
|
+
}
|
|
117
|
+
function buildWeaverseNextRequestInfo(context) {
|
|
118
|
+
let searchParams = getRequestInfoSearchParams(context);
|
|
119
|
+
let search = searchParams.toString();
|
|
120
|
+
return {
|
|
121
|
+
pathname: getPathnameFromContext(context),
|
|
122
|
+
search: search ? `?${search}` : "",
|
|
123
|
+
queries: toQueryRecord(searchParams),
|
|
124
|
+
...context?.i18n ? { i18n: context.i18n } : {},
|
|
125
|
+
...context?.pageType ? { pageType: context.pageType } : {},
|
|
126
|
+
...context?.handle ? { handle: context.handle } : {}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/revalidate-item.ts
|
|
131
|
+
const DEFAULT_REVALIDATE_ENDPOINT = "/api/weaverse/revalidate";
|
|
132
|
+
const MAX_ROUTE_CONTEXT_HANDLE_LENGTH = 512;
|
|
133
|
+
const DENIED_ROUTE_CONTEXT_PARAMS = [
|
|
134
|
+
"weaverseprojectid",
|
|
135
|
+
"weaversehost",
|
|
136
|
+
"weaverseapikey",
|
|
137
|
+
"weaverseapibase",
|
|
138
|
+
"weaversepublicapibase",
|
|
139
|
+
"weaverseversion",
|
|
140
|
+
"projectid",
|
|
141
|
+
"weaversedraftitem",
|
|
142
|
+
"__weaversedraftitem",
|
|
143
|
+
"_rsc"
|
|
144
|
+
];
|
|
145
|
+
const ROUTE_CONTEXT_I18N_KEYS = [
|
|
146
|
+
"country",
|
|
147
|
+
"label",
|
|
148
|
+
"language",
|
|
149
|
+
"locale",
|
|
150
|
+
"pathPrefix"
|
|
151
|
+
];
|
|
152
|
+
function sanitizeRouteContextSearch(search) {
|
|
153
|
+
let params = new URLSearchParams(search);
|
|
154
|
+
let denied = new Set(DENIED_ROUTE_CONTEXT_PARAMS);
|
|
155
|
+
for (let key of [...new Set(params.keys())]) if (denied.has(key.toLowerCase())) params.delete(key);
|
|
156
|
+
let serialized = params.toString();
|
|
157
|
+
return serialized ? `?${serialized}` : "";
|
|
158
|
+
}
|
|
159
|
+
function buildWeaverseNextRevalidateRouteContext(requestInfo) {
|
|
160
|
+
if (!requestInfo || typeof requestInfo.pathname !== "string" || !requestInfo.pathname) return;
|
|
161
|
+
let routeContext = {
|
|
162
|
+
pathname: requestInfo.pathname,
|
|
163
|
+
search: sanitizeRouteContextSearch(requestInfo.search ?? "")
|
|
164
|
+
};
|
|
165
|
+
let { i18n } = requestInfo;
|
|
166
|
+
if (i18n && typeof i18n === "object") {
|
|
167
|
+
let picked = {};
|
|
168
|
+
for (let key of ROUTE_CONTEXT_I18N_KEYS) {
|
|
169
|
+
let value = i18n[key];
|
|
170
|
+
if (typeof value === "string") picked[key] = value;
|
|
171
|
+
}
|
|
172
|
+
if (Object.keys(picked).length) routeContext.i18n = picked;
|
|
173
|
+
}
|
|
174
|
+
if (requestInfo.pageType && PageTypeSchema.safeParse(requestInfo.pageType).success) routeContext.pageType = requestInfo.pageType;
|
|
175
|
+
if (typeof requestInfo.handle === "string" && requestInfo.handle && requestInfo.handle.length <= MAX_ROUTE_CONTEXT_HANDLE_LENGTH) routeContext.handle = requestInfo.handle;
|
|
176
|
+
return routeContext;
|
|
177
|
+
}
|
|
178
|
+
async function revalidateWeaverseNextItem(runtime, draftItem, endpoint = DEFAULT_REVALIDATE_ENDPOINT) {
|
|
179
|
+
let instance = runtime.itemInstances.get(draftItem.id);
|
|
180
|
+
if (!instance) throw new Error(`[weaverse-next] No live item instance for "${draftItem.id}".`);
|
|
181
|
+
let routeContext = buildWeaverseNextRevalidateRouteContext(runtime.requestInfo);
|
|
182
|
+
let response = await fetch(endpoint, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
headers: { "Content-Type": "application/json" },
|
|
185
|
+
body: JSON.stringify(routeContext ? {
|
|
186
|
+
draftItem,
|
|
187
|
+
routeContext
|
|
188
|
+
} : { draftItem })
|
|
189
|
+
});
|
|
190
|
+
if (!response.ok) throw new Error(`[weaverse-next] Revalidate endpoint responded ${response.status}.`);
|
|
191
|
+
let { loaderData } = await response.json();
|
|
192
|
+
instance.setData({ loaderData: loaderData ?? null });
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/seo.ts
|
|
196
|
+
function hasOpenGraphContent(seo) {
|
|
197
|
+
return Boolean(seo?.title || seo?.description || seo?.image || seo?.type);
|
|
198
|
+
}
|
|
199
|
+
function hasTwitterContent(seo) {
|
|
200
|
+
return Boolean(seo?.title || seo?.description || seo?.image || seo?.cardType);
|
|
201
|
+
}
|
|
202
|
+
function normalizeTwitterCard(cardType) {
|
|
203
|
+
return cardType === "summary_large_image" ? "summary_large_image" : "summary";
|
|
204
|
+
}
|
|
205
|
+
function normalizeOpenGraphType(type) {
|
|
206
|
+
if (type === "article" || type === "profile" || type === "video.other") return type;
|
|
207
|
+
return "website";
|
|
208
|
+
}
|
|
209
|
+
function formatWeaverseNextSeoMetadata(seo) {
|
|
210
|
+
let metadata = { robots: {
|
|
211
|
+
index: seo?.robots?.index !== false,
|
|
212
|
+
follow: seo?.robots?.follow !== false
|
|
213
|
+
} };
|
|
214
|
+
if (!seo) return metadata;
|
|
215
|
+
if (seo.title) metadata.title = seo.title;
|
|
216
|
+
if (seo.description) metadata.description = seo.description;
|
|
217
|
+
if (seo.keywords) metadata.keywords = seo.keywords;
|
|
218
|
+
if (seo.canonicalUrl) metadata.alternates = { canonical: seo.canonicalUrl };
|
|
219
|
+
let openGraph = seo.openGraph;
|
|
220
|
+
if (hasOpenGraphContent(openGraph)) metadata.openGraph = {
|
|
221
|
+
title: openGraph?.title,
|
|
222
|
+
description: openGraph?.description,
|
|
223
|
+
images: openGraph?.image ? [openGraph.image] : void 0,
|
|
224
|
+
type: normalizeOpenGraphType(openGraph?.type)
|
|
225
|
+
};
|
|
226
|
+
let twitter = seo.twitter;
|
|
227
|
+
if (hasTwitterContent(twitter)) metadata.twitter = {
|
|
228
|
+
card: normalizeTwitterCard(twitter?.cardType),
|
|
229
|
+
title: twitter?.title,
|
|
230
|
+
description: twitter?.description,
|
|
231
|
+
images: twitter?.image ? [twitter.image] : void 0
|
|
232
|
+
};
|
|
233
|
+
return metadata;
|
|
234
|
+
}
|
|
235
|
+
function getWeaverseNextSeoMetadata(data) {
|
|
236
|
+
if (!data) return formatWeaverseNextSeoMetadata(null);
|
|
237
|
+
return formatWeaverseNextSeoMetadata(("items" in data && "id" in data ? data : data.page ?? null)?.seo ?? null);
|
|
238
|
+
}
|
|
239
|
+
//#endregion
|
|
240
|
+
export { buildWeaverseNextRevalidateRouteContext as a, buildWeaverseNextRequestInfo as c, generateDataFromSchema as d, isBrowser as f, ROUTE_CONTEXT_I18N_KEYS as i, withPathPrefix as l, getWeaverseNextSeoMetadata as n, revalidateWeaverseNextItem as o, isIframe as p, DEFAULT_REVALIDATE_ENDPOINT as r, sanitizeRouteContextSearch as s, formatWeaverseNextSeoMetadata as t, runWeaverseComponentLoaders as u };
|
|
241
|
+
|
|
242
|
+
//# sourceMappingURL=seo-C-S2viuI.mjs.map
|