@uniweb/runtime 0.16.0 → 0.17.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/dist/ssr.js +115 -43
- package/dist/ssr.js.map +1 -1
- package/package.json +3 -3
- package/src/background-shared.js +72 -0
- package/src/collect-records.js +99 -0
- package/src/components/Background.jsx +4 -39
- package/src/default-fetcher.js +124 -52
- package/src/isolate-api.js +48 -1
- package/src/prefetch.js +7 -7
- package/src/ssr-renderer.js +2 -32
- package/src/ssr.js +8 -1
package/dist/ssr.js
CHANGED
|
@@ -2,6 +2,7 @@ import { isRichSchema, deriveCacheKey, resolveDefaultLocale, hasDarkScheme, crea
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { renderToString } from "react-dom/server";
|
|
4
4
|
import { sectionDomId } from "@uniweb/core/section-id";
|
|
5
|
+
import { applyBasePath } from "@uniweb/core/base-path";
|
|
5
6
|
import { routePatternToRegex, decodeRouteValue, splitPathCapture } from "@uniweb/core/route-match";
|
|
6
7
|
import { DEFAULT_ICON_BASE, iconUrl } from "@uniweb/core/icon-corpus";
|
|
7
8
|
import { buildTheme, FONT_LINKS_MARKER, buildSectionOverrides } from "@uniweb/theming";
|
|
@@ -11,6 +12,7 @@ import { resolveFetchConfigs } from "@uniweb/core/fetch-config";
|
|
|
11
12
|
import { deriveCacheKey as deriveCacheKey$1 } from "@uniweb/core/datastore";
|
|
12
13
|
import { buildDetailConfig } from "@uniweb/core/detail-url";
|
|
13
14
|
import { resolveDefaultLocale as resolveDefaultLocale$1 } from "@uniweb/core/locale-config";
|
|
15
|
+
import { resolveRecordsService } from "@uniweb/core/records-service";
|
|
14
16
|
function guaranteeItemStructure(item) {
|
|
15
17
|
return {
|
|
16
18
|
title: item.title || "",
|
|
@@ -248,6 +250,23 @@ function getComponentMeta(componentName) {
|
|
|
248
250
|
function getComponentDefaults(componentName) {
|
|
249
251
|
return globalThis.uniweb?.getComponentDefaults?.(componentName) || {};
|
|
250
252
|
}
|
|
253
|
+
function siteUrl(url) {
|
|
254
|
+
return applyBasePath(url, globalThis.uniweb?.activeWebsite?.basePath || "");
|
|
255
|
+
}
|
|
256
|
+
function withOpacity(color, opacity) {
|
|
257
|
+
if (typeof color !== "string" || !color) return color;
|
|
258
|
+
if (color.startsWith("#")) {
|
|
259
|
+
const r = parseInt(color.slice(1, 3), 16);
|
|
260
|
+
const g = parseInt(color.slice(3, 5), 16);
|
|
261
|
+
const b = parseInt(color.slice(5, 7), 16);
|
|
262
|
+
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
|
263
|
+
}
|
|
264
|
+
if (color.startsWith("rgb")) {
|
|
265
|
+
const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
266
|
+
if (match) return `rgba(${match[1]}, ${match[2]}, ${match[3]}, ${opacity})`;
|
|
267
|
+
}
|
|
268
|
+
return color;
|
|
269
|
+
}
|
|
251
270
|
function default404Html(basePath = "") {
|
|
252
271
|
const homeHref = basePath ? `${basePath}/` : "/";
|
|
253
272
|
return `<div class="page-not-found" style="min-height:80vh;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem;text-align:center"><h1 style="font-size:3rem;font-weight:bold;color:#1f2937;margin-bottom:1rem">404</h1><p style="color:#64748b;margin-bottom:2rem">Page not found</p><a href="${homeHref}" style="color:#3b82f6;text-decoration:underline">Go to homepage</a></div>`;
|
|
@@ -394,28 +413,6 @@ function getWrapperProps(block) {
|
|
|
394
413
|
}
|
|
395
414
|
return { id: sectionDomId(block), style, className, background };
|
|
396
415
|
}
|
|
397
|
-
function withOpacity(color, opacity) {
|
|
398
|
-
if (color.startsWith("#")) {
|
|
399
|
-
const r = parseInt(color.slice(1, 3), 16);
|
|
400
|
-
const g = parseInt(color.slice(3, 5), 16);
|
|
401
|
-
const b = parseInt(color.slice(5, 7), 16);
|
|
402
|
-
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
|
403
|
-
}
|
|
404
|
-
if (color.startsWith("rgb")) {
|
|
405
|
-
const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
406
|
-
if (match) {
|
|
407
|
-
return `rgba(${match[1]}, ${match[2]}, ${match[3]}, ${opacity})`;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
return color;
|
|
411
|
-
}
|
|
412
|
-
function resolveUrl(url) {
|
|
413
|
-
if (!url || !url.startsWith("/")) return url;
|
|
414
|
-
const basePath = globalThis.uniweb?.activeWebsite?.basePath || "";
|
|
415
|
-
if (!basePath) return url;
|
|
416
|
-
if (url.startsWith(basePath + "/") || url === basePath) return url;
|
|
417
|
-
return basePath + url;
|
|
418
|
-
}
|
|
419
416
|
function renderBackground(background) {
|
|
420
417
|
if (!background?.mode) return null;
|
|
421
418
|
const containerStyle = {
|
|
@@ -472,7 +469,7 @@ function renderBackground(background) {
|
|
|
472
469
|
style: {
|
|
473
470
|
position: "absolute",
|
|
474
471
|
inset: "0",
|
|
475
|
-
backgroundImage: `url(${
|
|
472
|
+
backgroundImage: `url(${siteUrl(img.src)})`,
|
|
476
473
|
backgroundPosition: img.position || "center",
|
|
477
474
|
backgroundSize: img.size || "cover",
|
|
478
475
|
backgroundRepeat: "no-repeat"
|
|
@@ -842,23 +839,23 @@ function generate404Html({ baseHtml, website, siteContent }) {
|
|
|
842
839
|
function createDefaultFetcher({ basePath = "", dev = false, fetch: fetchImpl = null } = {}) {
|
|
843
840
|
const doFetch = (input, init) => (fetchImpl || globalThis.fetch)(input, init);
|
|
844
841
|
const pathPrefix = basePath && basePath !== "/" ? basePath.replace(/\/$/, "") : "";
|
|
845
|
-
const
|
|
846
|
-
const
|
|
842
|
+
const askQueues = /* @__PURE__ */ new Map();
|
|
843
|
+
const askRecords = (request, ctx) => {
|
|
847
844
|
if (typeof request.schema !== "string" || !request.schema) {
|
|
848
845
|
return Promise.resolve({
|
|
849
846
|
data: null,
|
|
850
|
-
error: `the payload
|
|
847
|
+
error: `the payload offers the records service but carries no Model ref for query "${request.query ?? request.as}" (config.queries) — it cannot be asked`
|
|
851
848
|
});
|
|
852
849
|
}
|
|
853
850
|
return new Promise((resolve) => {
|
|
854
|
-
const url = resolveServiceUrl(request.
|
|
855
|
-
let queue =
|
|
851
|
+
const url = resolveServiceUrl(request.ask, pathPrefix);
|
|
852
|
+
let queue = askQueues.get(url);
|
|
856
853
|
if (!queue) {
|
|
857
854
|
queue = [];
|
|
858
|
-
|
|
855
|
+
askQueues.set(url, queue);
|
|
859
856
|
queueMicrotask(() => {
|
|
860
|
-
|
|
861
|
-
|
|
857
|
+
askQueues.delete(url);
|
|
858
|
+
flushAsked(url, queue, doFetch);
|
|
862
859
|
});
|
|
863
860
|
}
|
|
864
861
|
queue.push({ request, ctx, resolve });
|
|
@@ -866,8 +863,8 @@ function createDefaultFetcher({ basePath = "", dev = false, fetch: fetchImpl = n
|
|
|
866
863
|
};
|
|
867
864
|
return {
|
|
868
865
|
/**
|
|
869
|
-
* The cache identity is the request's ADDRESS — or,
|
|
870
|
-
* the QUESTION (`deriveCacheKey` hashes every operator of an address-less
|
|
866
|
+
* The cache identity is the request's ADDRESS — or, when asked of the
|
|
867
|
+
* records service, the QUESTION (`deriveCacheKey` hashes every operator of an address-less
|
|
871
868
|
* request). Operators evaluated here run over a shared cached value and
|
|
872
869
|
* must NOT split the cache: two pages declaring different `where:` clauses
|
|
873
870
|
* against the same path share one entry — the file is fetched once and
|
|
@@ -878,7 +875,7 @@ function createDefaultFetcher({ basePath = "", dev = false, fetch: fetchImpl = n
|
|
|
878
875
|
},
|
|
879
876
|
async resolve(request, ctx = {}) {
|
|
880
877
|
if (!request) return { data: null };
|
|
881
|
-
if (request.
|
|
878
|
+
if (request.ask) return askRecords(request, ctx);
|
|
882
879
|
const { path, url, transform, body: rawBody } = request;
|
|
883
880
|
let method = (request.method || "GET").toUpperCase();
|
|
884
881
|
if (method !== "GET" && method !== "POST") {
|
|
@@ -891,7 +888,7 @@ function createDefaultFetcher({ basePath = "", dev = false, fetch: fetchImpl = n
|
|
|
891
888
|
} else if (url) {
|
|
892
889
|
target = url;
|
|
893
890
|
} else {
|
|
894
|
-
return { data: [], error: "No path, url or
|
|
891
|
+
return { data: [], error: "No path, url or ask specified" };
|
|
895
892
|
}
|
|
896
893
|
const init = { signal: ctx.signal, method };
|
|
897
894
|
if (method === "POST") {
|
|
@@ -954,7 +951,7 @@ function createDefaultFetcher({ basePath = "", dev = false, fetch: fetchImpl = n
|
|
|
954
951
|
}
|
|
955
952
|
};
|
|
956
953
|
}
|
|
957
|
-
function
|
|
954
|
+
function toQuestion(request) {
|
|
958
955
|
const q = { schema: request.schema };
|
|
959
956
|
let where = request.where && typeof request.where === "object" ? request.where : null;
|
|
960
957
|
let scope = typeof request.scope === "string" && request.scope ? request.scope : null;
|
|
@@ -969,19 +966,22 @@ function doorQuestion(request) {
|
|
|
969
966
|
if (sort) q.sort = sort;
|
|
970
967
|
if (typeof request.limit === "number" && request.limit > 0) q.limit = request.limit;
|
|
971
968
|
if (request.depth === "brief" || request.depth === "full") q.depth = request.depth;
|
|
969
|
+
if (typeof request.cursor === "string" && request.cursor) q.cursor = request.cursor;
|
|
972
970
|
return q;
|
|
973
971
|
}
|
|
974
|
-
const
|
|
972
|
+
const OPERATOR_ALIAS = { nin: "not_in" };
|
|
975
973
|
function renameOperators(where) {
|
|
976
974
|
if (Array.isArray(where)) return where.map(renameOperators);
|
|
977
975
|
if (!where || typeof where !== "object") return where;
|
|
978
976
|
const out = {};
|
|
979
977
|
for (const [key, value] of Object.entries(where)) {
|
|
980
|
-
out[
|
|
978
|
+
out[OPERATOR_ALIAS[key] ?? key] = value && typeof value === "object" ? renameOperators(value) : value;
|
|
981
979
|
}
|
|
982
980
|
return out;
|
|
983
981
|
}
|
|
984
|
-
|
|
982
|
+
const MAX_PAGES = 50;
|
|
983
|
+
async function flushAsked(url, queue, doFetch) {
|
|
984
|
+
const pending = /* @__PURE__ */ new Map();
|
|
985
985
|
const body = {};
|
|
986
986
|
const keys = [];
|
|
987
987
|
for (const entry of queue) {
|
|
@@ -989,7 +989,7 @@ async function flushDoor(url, queue, doFetch) {
|
|
|
989
989
|
let key = base;
|
|
990
990
|
for (let n = 2; key in body; n += 1) key = `${base}#${n}`;
|
|
991
991
|
keys.push(key);
|
|
992
|
-
body[key] =
|
|
992
|
+
body[key] = toQuestion(entry.request);
|
|
993
993
|
}
|
|
994
994
|
let parsed;
|
|
995
995
|
try {
|
|
@@ -1018,6 +1018,8 @@ async function flushDoor(url, queue, doFetch) {
|
|
|
1018
1018
|
const data = parsed && typeof parsed.data === "object" && parsed.data ? parsed.data : {};
|
|
1019
1019
|
const errors = parsed && typeof parsed.errors === "object" && parsed.errors ? parsed.errors : {};
|
|
1020
1020
|
const depths = parsed && typeof parsed.depths === "object" && parsed.depths ? parsed.depths : {};
|
|
1021
|
+
const cursors = parsed && typeof parsed.cursors === "object" && parsed.cursors ? parsed.cursors : {};
|
|
1022
|
+
const limits = parsed && typeof parsed.limits === "object" && parsed.limits ? parsed.limits : {};
|
|
1021
1023
|
queue.forEach((entry, i) => {
|
|
1022
1024
|
const key = keys[i];
|
|
1023
1025
|
if (key in errors) {
|
|
@@ -1029,12 +1031,50 @@ async function flushDoor(url, queue, doFetch) {
|
|
|
1029
1031
|
return;
|
|
1030
1032
|
}
|
|
1031
1033
|
if (!(key in data)) {
|
|
1032
|
-
entry.resolve({ data: null, error: `the records
|
|
1034
|
+
entry.resolve({ data: null, error: `the records service answered without the key "${key}"` });
|
|
1033
1035
|
return;
|
|
1034
1036
|
}
|
|
1035
1037
|
const depth = depths[key] === "brief" || depths[key] === "full" ? depths[key] : entry.request.depth === "brief" || entry.request.depth === "full" ? entry.request.depth : void 0;
|
|
1036
|
-
|
|
1038
|
+
const cursor = typeof cursors[key] === "string" && cursors[key] ? cursors[key] : null;
|
|
1039
|
+
const bound = typeof limits[key] === "number" ? limits[key] : void 0;
|
|
1040
|
+
const rows = Array.isArray(data[key]) ? data[key] : data[key];
|
|
1041
|
+
if (cursor && entry.request.exhaustive && Array.isArray(rows)) {
|
|
1042
|
+
const acc = entry.collected ? entry.collected.concat(rows) : rows.slice();
|
|
1043
|
+
const page = (entry.page || 1) + 1;
|
|
1044
|
+
if (page <= MAX_PAGES) {
|
|
1045
|
+
pending.set(entry, { cursor, collected: acc, page, depth, bound });
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
entry.resolve({ data: acc, meta: withMeta({ depth, bound, truncated: true, pages: MAX_PAGES }) });
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
const collected = entry.collected ? entry.collected.concat(Array.isArray(rows) ? rows : []) : rows;
|
|
1052
|
+
const meta = withMeta({
|
|
1053
|
+
depth,
|
|
1054
|
+
bound,
|
|
1055
|
+
// ⭐ A cursor IS the truncation signal, and it is the only one for a query
|
|
1056
|
+
// that declared no `limit`: `limits` is reported only when an author's own
|
|
1057
|
+
// limit was clamped (the records contract §5).
|
|
1058
|
+
truncated: cursor ? true : void 0,
|
|
1059
|
+
pages: entry.page && entry.page > 1 ? entry.page : void 0
|
|
1060
|
+
});
|
|
1061
|
+
entry.resolve(meta ? { data: collected, meta } : { data: collected });
|
|
1037
1062
|
});
|
|
1063
|
+
if (pending.size === 0) return;
|
|
1064
|
+
await Promise.all([...pending].map(([entry, state]) => {
|
|
1065
|
+
const next = {
|
|
1066
|
+
...entry,
|
|
1067
|
+
request: { ...entry.request, cursor: state.cursor },
|
|
1068
|
+
collected: state.collected,
|
|
1069
|
+
page: state.page
|
|
1070
|
+
};
|
|
1071
|
+
return flushAsked(url, [next], doFetch);
|
|
1072
|
+
}));
|
|
1073
|
+
}
|
|
1074
|
+
function withMeta(fields) {
|
|
1075
|
+
const out = {};
|
|
1076
|
+
for (const [k, v] of Object.entries(fields)) if (v !== void 0) out[k] = v;
|
|
1077
|
+
return Object.keys(out).length ? out : void 0;
|
|
1038
1078
|
}
|
|
1039
1079
|
function applyOperators(data, request, { dev = false } = {}) {
|
|
1040
1080
|
if (!Array.isArray(data)) return data;
|
|
@@ -1114,7 +1154,7 @@ function resolvePageFetchConfigs(content, route, { locale = null } = {}) {
|
|
|
1114
1154
|
locale,
|
|
1115
1155
|
defaultLocale: resolveDefaultLocale$1(content?.config) ?? null,
|
|
1116
1156
|
queries: content?.config?.queries ?? null,
|
|
1117
|
-
|
|
1157
|
+
services: content?.config?.services ?? null,
|
|
1118
1158
|
variables: binding?.variables ?? null
|
|
1119
1159
|
};
|
|
1120
1160
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -1199,10 +1239,42 @@ async function prefetchAndHydrate({ website, content, route, locale = null, fetc
|
|
|
1199
1239
|
hydrateDataStore(website, fetched);
|
|
1200
1240
|
return fetched;
|
|
1201
1241
|
}
|
|
1242
|
+
async function collectSiteRecords(content, { locale, fetch: fetch2, signal, only = null } = {}) {
|
|
1243
|
+
const config = content?.config;
|
|
1244
|
+
const services = config?.services ?? null;
|
|
1245
|
+
const queries = config?.queries ?? null;
|
|
1246
|
+
if (!queries || typeof queries !== "object") return empty();
|
|
1247
|
+
if (!resolveRecordsService(services, locale)) return empty();
|
|
1248
|
+
const names = Object.keys(queries).filter((n) => only ? only.includes(n) : true);
|
|
1249
|
+
if (names.length === 0) return empty();
|
|
1250
|
+
const configs = resolveFetchConfigs(
|
|
1251
|
+
names.map((name) => ({ query: name, as: name })),
|
|
1252
|
+
{ services, queries, locale, defaultLocale: config?.defaultLanguage ?? locale }
|
|
1253
|
+
);
|
|
1254
|
+
const fetcher = createDefaultFetcher({ fetch: fetch2, basePath: config?.base ?? "" });
|
|
1255
|
+
const records = {};
|
|
1256
|
+
const errors = {};
|
|
1257
|
+
const meta = {};
|
|
1258
|
+
await Promise.all([...configs].map(async ([name, cfg]) => {
|
|
1259
|
+
if (!cfg.ask) return;
|
|
1260
|
+
const result = await fetcher.resolve({ ...cfg, depth: "brief", exhaustive: true }, { signal });
|
|
1261
|
+
if (result?.error) {
|
|
1262
|
+
errors[name] = result.error;
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
records[name] = Array.isArray(result?.data) ? result.data : [];
|
|
1266
|
+
if (result?.meta) meta[name] = result.meta;
|
|
1267
|
+
}));
|
|
1268
|
+
return { records, errors: Object.keys(errors).length ? errors : null, meta };
|
|
1269
|
+
}
|
|
1270
|
+
function empty() {
|
|
1271
|
+
return { records: {}, errors: null, meta: {} };
|
|
1272
|
+
}
|
|
1202
1273
|
export {
|
|
1203
1274
|
applyDefaults,
|
|
1204
1275
|
applySchemas,
|
|
1205
1276
|
classifyRenderError,
|
|
1277
|
+
collectSiteRecords,
|
|
1206
1278
|
createPageRenderer,
|
|
1207
1279
|
escapeHtml,
|
|
1208
1280
|
executeFetchConfigs,
|