@uniweb/runtime 0.17.0 → 0.17.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/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";
@@ -249,6 +250,23 @@ function getComponentMeta(componentName) {
249
250
  function getComponentDefaults(componentName) {
250
251
  return globalThis.uniweb?.getComponentDefaults?.(componentName) || {};
251
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
+ }
252
270
  function default404Html(basePath = "") {
253
271
  const homeHref = basePath ? `${basePath}/` : "/";
254
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>`;
@@ -395,28 +413,6 @@ function getWrapperProps(block) {
395
413
  }
396
414
  return { id: sectionDomId(block), style, className, background };
397
415
  }
398
- function withOpacity(color, opacity) {
399
- if (color.startsWith("#")) {
400
- const r = parseInt(color.slice(1, 3), 16);
401
- const g = parseInt(color.slice(3, 5), 16);
402
- const b = parseInt(color.slice(5, 7), 16);
403
- return `rgba(${r}, ${g}, ${b}, ${opacity})`;
404
- }
405
- if (color.startsWith("rgb")) {
406
- const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
407
- if (match) {
408
- return `rgba(${match[1]}, ${match[2]}, ${match[3]}, ${opacity})`;
409
- }
410
- }
411
- return color;
412
- }
413
- function resolveUrl(url) {
414
- if (!url || !url.startsWith("/")) return url;
415
- const basePath = globalThis.uniweb?.activeWebsite?.basePath || "";
416
- if (!basePath) return url;
417
- if (url.startsWith(basePath + "/") || url === basePath) return url;
418
- return basePath + url;
419
- }
420
416
  function renderBackground(background) {
421
417
  if (!background?.mode) return null;
422
418
  const containerStyle = {
@@ -473,7 +469,7 @@ function renderBackground(background) {
473
469
  style: {
474
470
  position: "absolute",
475
471
  inset: "0",
476
- backgroundImage: `url(${resolveUrl(img.src)})`,
472
+ backgroundImage: `url(${siteUrl(img.src)})`,
477
473
  backgroundPosition: img.position || "center",
478
474
  backgroundSize: img.size || "cover",
479
475
  backgroundRepeat: "no-repeat"
@@ -983,7 +979,7 @@ function renameOperators(where) {
983
979
  }
984
980
  return out;
985
981
  }
986
- const MAX_PAGES = 50;
982
+ const DEFAULT_MAX_PAGES = 20;
987
983
  async function flushAsked(url, queue, doFetch) {
988
984
  const pending = /* @__PURE__ */ new Map();
989
985
  const body = {};
@@ -1016,7 +1012,10 @@ async function flushAsked(url, queue, doFetch) {
1016
1012
  parsed = await response.json();
1017
1013
  } catch (error) {
1018
1014
  const message = error?.name === "AbortError" ? "aborted" : error?.message || String(error);
1019
- for (const entry of queue) entry.resolve({ data: null, error: message });
1015
+ for (const entry of queue) {
1016
+ const held = Array.isArray(entry.collected) && entry.collected.length ? entry.collected : null;
1017
+ entry.resolve(held ? { data: held, error: message, meta: withMeta({ partial: true, pages: entry.page }) } : { data: null, error: message });
1018
+ }
1020
1019
  return;
1021
1020
  }
1022
1021
  const data = parsed && typeof parsed.data === "object" && parsed.data ? parsed.data : {};
@@ -1026,10 +1025,11 @@ async function flushAsked(url, queue, doFetch) {
1026
1025
  const limits = parsed && typeof parsed.limits === "object" && parsed.limits ? parsed.limits : {};
1027
1026
  queue.forEach((entry, i) => {
1028
1027
  const key = keys[i];
1028
+ const held = Array.isArray(entry.collected) && entry.collected.length ? entry.collected : null;
1029
1029
  if (key in errors) {
1030
1030
  const e = errors[key];
1031
1031
  const detail = typeof e === "string" ? e : e?.detail || e?.message || JSON.stringify(e);
1032
- const out = { data: null, error: detail };
1032
+ const out = held ? { data: held, error: detail, meta: withMeta({ partial: true, pages: entry.page }) } : { data: null, error: detail };
1033
1033
  if (e && typeof e === "object" && typeof e.code === "string") out.code = e.code;
1034
1034
  entry.resolve(out);
1035
1035
  return;
@@ -1045,21 +1045,32 @@ async function flushAsked(url, queue, doFetch) {
1045
1045
  if (cursor && entry.request.exhaustive && Array.isArray(rows)) {
1046
1046
  const acc = entry.collected ? entry.collected.concat(rows) : rows.slice();
1047
1047
  const page = (entry.page || 1) + 1;
1048
- if (page <= MAX_PAGES) {
1048
+ const cap = typeof entry.request.maxPages === "number" && entry.request.maxPages > 0 ? entry.request.maxPages : DEFAULT_MAX_PAGES;
1049
+ if (page <= cap) {
1049
1050
  pending.set(entry, { cursor, collected: acc, page, depth, bound });
1050
1051
  return;
1051
1052
  }
1052
- entry.resolve({ data: acc, meta: withMeta({ depth, bound, truncated: true, pages: MAX_PAGES }) });
1053
+ entry.resolve({ data: acc, meta: withMeta({ depth, bound, partial: true, pages: cap }) });
1053
1054
  return;
1054
1055
  }
1055
1056
  const collected = entry.collected ? entry.collected.concat(Array.isArray(rows) ? rows : []) : rows;
1056
1057
  const meta = withMeta({
1057
1058
  depth,
1058
1059
  bound,
1059
- // ⭐ A cursor IS the truncation signal, and it is the only one for a query
1060
- // that declared no `limit`: `limits` is reported only when an author's own
1061
- // limit was clamped (the records contract §5).
1062
- truncated: cursor ? true : void 0,
1060
+ // ⭐ ONE FLAG, MEANING **NOT THE WHOLE POPULATION** asked for by name, so
1061
+ // a caller has one boolean to branch on rather than three signals to
1062
+ // combine. It is set in every case that means it:
1063
+ // · a cursor came back and this caller does not page (a page render);
1064
+ // · a cursor came back and the caller's `maxPages` stopped the walk;
1065
+ // · the walk failed or aborted with pages already in hand;
1066
+ // · the service reported it BOUNDED the answer and offered no cursor.
1067
+ // ⚠️ The last is why `limits` is read at all: a cursor is the signal for a
1068
+ // query that declared no `limit`, and `limits` is the signal for one whose
1069
+ // author limit was clamped (the records contract §5). Neither alone covers
1070
+ // both. ⛔ Named `truncated` when it shipped in 0.17.0 this morning; renamed
1071
+ // the same day, before any consumer adopted it, because "truncated" says
1072
+ // something was cut and this also means "there is more you did not ask for".
1073
+ partial: cursor || bound !== void 0 ? true : void 0,
1063
1074
  pages: entry.page && entry.page > 1 ? entry.page : void 0
1064
1075
  });
1065
1076
  entry.resolve(meta ? { data: collected, meta } : { data: collected });
@@ -1243,7 +1254,7 @@ async function prefetchAndHydrate({ website, content, route, locale = null, fetc
1243
1254
  hydrateDataStore(website, fetched);
1244
1255
  return fetched;
1245
1256
  }
1246
- async function collectSiteRecords(content, { locale, fetch: fetch2, signal, only = null } = {}) {
1257
+ async function collectSiteRecords(content, { locale, fetch: fetch2, signal, only = null, depth = "brief", maxPages } = {}) {
1247
1258
  const config = content?.config;
1248
1259
  const services = config?.services ?? null;
1249
1260
  const queries = config?.queries ?? null;
@@ -1261,12 +1272,13 @@ async function collectSiteRecords(content, { locale, fetch: fetch2, signal, only
1261
1272
  const meta = {};
1262
1273
  await Promise.all([...configs].map(async ([name, cfg]) => {
1263
1274
  if (!cfg.ask) return;
1264
- const result = await fetcher.resolve({ ...cfg, depth: "brief", exhaustive: true }, { signal });
1265
- if (result?.error) {
1266
- errors[name] = result.error;
1267
- return;
1268
- }
1269
- records[name] = Array.isArray(result?.data) ? result.data : [];
1275
+ const { limit, ...population } = cfg;
1276
+ const asked = { ...population, depth, exhaustive: true };
1277
+ if (typeof maxPages === "number" && maxPages > 0) asked.maxPages = maxPages;
1278
+ const result = await fetcher.resolve(asked, { signal });
1279
+ if (result?.error) errors[name] = result.error;
1280
+ if (Array.isArray(result?.data)) records[name] = result.data;
1281
+ else if (!result?.error) records[name] = [];
1270
1282
  if (result?.meta) meta[name] = result.meta;
1271
1283
  }));
1272
1284
  return { records, errors: Object.keys(errors).length ? errors : null, meta };