@uniweb/runtime 0.22.0 → 0.24.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.
package/dist/ssr.js CHANGED
@@ -9,10 +9,11 @@ import { DEFAULT_ICON_BASE, iconUrl } from "@uniweb/core/icon-corpus";
9
9
  import { buildTheme, FONT_LINKS_MARKER, buildSectionOverrides } from "@uniweb/theming";
10
10
  import "@uniweb/core/services";
11
11
  import "@uniweb/core/tracker";
12
- import { routeQuery, sectionFetches, resolveFetchConfigs } from "@uniweb/core/fetch-config";
12
+ import { pageRouteQuery, siteReaches, resolveFetchConfigs, currentOf, othersView, routeSelection } from "@uniweb/core/fetch-config";
13
13
  import { deriveCacheKey as deriveCacheKey$1 } from "@uniweb/core/datastore";
14
14
  import { buildDetailConfig } from "@uniweb/core/detail-url";
15
15
  import { resolveDefaultLocale as resolveDefaultLocale$1 } from "@uniweb/core/locale-config";
16
+ import { findLayoutEntry } from "@uniweb/core/layout-name";
16
17
  import { resolveRecordsService } from "@uniweb/core/records-service";
17
18
  function guaranteeItemStructure(item) {
18
19
  return {
@@ -911,30 +912,14 @@ function createDefaultFetcher({ basePath = "", dev = false, fetch: fetchImpl = n
911
912
  try {
912
913
  const response = await readOnce(target, init);
913
914
  if (ctx.signal?.aborted) return { data: [], error: "aborted" };
914
- const envelope = request.envelope && typeof request.envelope === "object" ? request.envelope : {};
915
915
  if (!response.ok) {
916
- let extracted;
917
- if (envelope.error && typeof response.text === "string") {
918
- const body = safeParseJSON(response.text);
919
- if (body !== void 0) {
920
- const candidate = getNestedValue(body, envelope.error);
921
- if (typeof candidate === "string" && candidate.length) {
922
- extracted = candidate;
923
- }
924
- }
925
- }
926
- return {
927
- data: [],
928
- error: extracted ?? `HTTP ${response.status}: ${response.statusText}`
929
- };
916
+ return { data: [], error: `HTTP ${response.status}: ${response.statusText}` };
930
917
  }
931
918
  let data = response.body;
932
- const isDetailRequest = !!request.dynamicContext;
933
- const effectiveTransform = transform || (isDetailRequest ? envelope.item : envelope.list);
934
- if (effectiveTransform && data !== null && data !== void 0) {
935
- data = getNestedValue(data, effectiveTransform);
919
+ if (transform && data !== null && data !== void 0) {
920
+ data = getNestedValue(data, transform);
936
921
  }
937
- data = applyOperators(data, request, { dev });
922
+ data = applyOperators(data, request, { dev, locale: sortLocale(request, ctx) });
938
923
  const whole = typeof request.whole === "boolean" ? request.whole : void 0;
939
924
  return whole === void 0 ? { data: data ?? [] } : { data: data ?? [], meta: { whole } };
940
925
  } catch (error) {
@@ -951,7 +936,7 @@ function toQuestion(request) {
951
936
  const where = request.where && typeof request.where === "object" ? request.where : null;
952
937
  const scope = typeof request.scope === "string" && request.scope ? request.scope : null;
953
938
  if (scope) q.scope = scope;
954
- if (where) q.where = renameOperators(where);
939
+ if (where) q.where = where;
955
940
  if (request.match && typeof request.match === "object") q.match = request.match;
956
941
  const sort = sortToWire(request.sort);
957
942
  if (sort) q.sort = sort;
@@ -960,16 +945,6 @@ function toQuestion(request) {
960
945
  if (typeof request.cursor === "string" && request.cursor) q.cursor = request.cursor;
961
946
  return q;
962
947
  }
963
- const OPERATOR_ALIAS = { nin: "not_in" };
964
- function renameOperators(where) {
965
- if (Array.isArray(where)) return where.map(renameOperators);
966
- if (!where || typeof where !== "object") return where;
967
- const out = {};
968
- for (const [key, value] of Object.entries(where)) {
969
- out[OPERATOR_ALIAS[key] ?? key] = value && typeof value === "object" ? renameOperators(value) : value;
970
- }
971
- return out;
972
- }
973
948
  const DEFAULT_MAX_PAGES = 20;
974
949
  async function flushAsked(url, queue, doFetch) {
975
950
  const pending = /* @__PURE__ */ new Map();
@@ -1010,58 +985,47 @@ async function flushAsked(url, queue, doFetch) {
1010
985
  return;
1011
986
  }
1012
987
  const data = parsed && typeof parsed.data === "object" && parsed.data ? parsed.data : {};
1013
- const errors = parsed && typeof parsed.errors === "object" && parsed.errors ? parsed.errors : {};
1014
988
  const wholes = parsed && typeof parsed.whole === "object" && parsed.whole ? parsed.whole : {};
1015
989
  const cursors = parsed && typeof parsed.cursors === "object" && parsed.cursors ? parsed.cursors : {};
1016
- const limits = parsed && typeof parsed.limits === "object" && parsed.limits ? parsed.limits : {};
990
+ const degraded = parsed && typeof parsed.partial === "object" && parsed.partial ? parsed.partial : {};
1017
991
  queue.forEach((entry, i) => {
1018
992
  const key = keys[i];
1019
993
  const held = Array.isArray(entry.collected) && entry.collected.length ? entry.collected : null;
1020
- if (key in errors) {
1021
- const e = errors[key];
1022
- const detail = typeof e === "string" ? e : e?.detail || e?.message || JSON.stringify(e);
1023
- const out = held ? { data: held, error: detail, meta: withMeta({ partial: true, pages: entry.page }) } : { data: null, error: detail };
1024
- if (e && typeof e === "object" && typeof e.code === "string") out.code = e.code;
1025
- entry.resolve(out);
1026
- return;
1027
- }
1028
994
  if (!(key in data)) {
1029
- entry.resolve({ data: null, error: `the records service answered without the key "${key}"` });
995
+ const error = `the records service answered without the key "${key}"`;
996
+ entry.resolve(held ? { data: held, error, meta: withMeta({ partial: true, pages: entry.page }) } : { data: null, error });
1030
997
  return;
1031
998
  }
1032
999
  const whole = typeof wholes[key] === "boolean" ? wholes[key] : typeof entry.request.whole === "boolean" ? entry.request.whole : void 0;
1033
1000
  const cursor = typeof cursors[key] === "string" && cursors[key] ? cursors[key] : null;
1034
- const bound = typeof limits[key] === "number" ? limits[key] : void 0;
1035
1001
  const rows = Array.isArray(data[key]) ? data[key] : data[key];
1036
1002
  if (cursor && entry.request.exhaustive && Array.isArray(rows)) {
1037
1003
  const acc = entry.collected ? entry.collected.concat(rows) : rows.slice();
1038
1004
  const page = (entry.page || 1) + 1;
1039
1005
  const cap = typeof entry.request.maxPages === "number" && entry.request.maxPages > 0 ? entry.request.maxPages : DEFAULT_MAX_PAGES;
1040
1006
  if (page <= cap) {
1041
- pending.set(entry, { cursor, collected: acc, page, whole, bound });
1007
+ pending.set(entry, { cursor, collected: acc, page, whole });
1042
1008
  return;
1043
1009
  }
1044
- entry.resolve({ data: acc, meta: withMeta({ whole, bound, partial: true, pages: cap }) });
1010
+ entry.resolve({ data: acc, meta: withMeta({ whole, partial: true, pages: cap }) });
1045
1011
  return;
1046
1012
  }
1047
1013
  const collected = entry.collected ? entry.collected.concat(Array.isArray(rows) ? rows : []) : rows;
1048
1014
  const meta = withMeta({
1049
1015
  whole,
1050
- bound,
1051
1016
  // ⭐ ONE FLAG, MEANING **NOT THE WHOLE POPULATION** — asked for by name, so
1052
- // a caller has one boolean to branch on rather than three signals to
1017
+ // a caller has one boolean to branch on rather than several signals to
1053
1018
  // combine. It is set in every case that means it:
1054
1019
  // · a cursor came back and this caller does not page (a page render);
1055
1020
  // · a cursor came back and the caller's `maxPages` stopped the walk;
1056
1021
  // · the walk failed or aborted with pages already in hand;
1057
- // · the service reported it BOUNDED the answer and offered no cursor.
1058
- // ⚠️ The last is why `limits` is read at all: a cursor is the signal for a
1059
- // query that declared no `limit`, and `limits` is the signal for one whose
1060
- // author limit was clamped (the records contract §5). Neither alone covers
1061
- // both. ⛔ Named `truncated` when it shipped in 0.17.0 this morning; renamed
1062
- // the same day, before any consumer adopted it, because "truncated" says
1063
- // something was cut and this also means "there is more you did not ask for".
1064
- partial: cursor || bound !== void 0 ? true : void 0,
1022
+ // · a source of the key did not answer (`partial[key]`), and `unavailable`
1023
+ // carries the service's own account of which one.
1024
+ // ⛔ Named `truncated` when it shipped in 0.17.0; renamed the same day, before
1025
+ // any consumer adopted it, because "truncated" says something was cut and this
1026
+ // also means "there is more you did not ask for".
1027
+ partial: cursor || key in degraded ? true : void 0,
1028
+ unavailable: key in degraded ? degraded[key] : void 0,
1065
1029
  pages: entry.page && entry.page > 1 ? entry.page : void 0
1066
1030
  });
1067
1031
  entry.resolve(meta ? { data: collected, meta } : { data: collected });
@@ -1101,19 +1065,25 @@ function withMeta(fields) {
1101
1065
  for (const [k, v] of Object.entries(fields)) if (v !== void 0) out[k] = v;
1102
1066
  return Object.keys(out).length ? out : void 0;
1103
1067
  }
1104
- function applyOperators(data, request, { dev = false } = {}) {
1068
+ function sortLocale(request, ctx) {
1069
+ if (typeof request?.locale === "string" && request.locale) return request.locale;
1070
+ if (typeof ctx?.locale === "string" && ctx.locale) return ctx.locale;
1071
+ const active = ctx?.website?.getActiveLocale?.();
1072
+ return typeof active === "string" && active ? active : null;
1073
+ }
1074
+ function applyOperators(data, request, { dev = false, locale = null } = {}) {
1105
1075
  if (!Array.isArray(data)) return data;
1106
1076
  let result = data;
1107
1077
  if (typeof request.scope === "string" && request.scope) result = applyScope(result, request.scope);
1108
1078
  if (request.where) result = matchWhere(request.where, result);
1109
- if (request.sort) result = applySort(result, request.sort, dev);
1079
+ if (request.sort) result = applySort(result, request.sort, dev, locale);
1110
1080
  if (typeof request.limit === "number" && request.limit > 0) result = result.slice(0, request.limit);
1111
1081
  return result;
1112
1082
  }
1113
1083
  const warnedBadSorts = /* @__PURE__ */ new Set();
1114
- function applySort(items, sortExpr, dev) {
1084
+ function applySort(items, sortExpr, dev, locale) {
1115
1085
  try {
1116
- return sortRecords(items, sortExpr);
1086
+ return sortRecords(items, sortExpr, { locale });
1117
1087
  } catch (err) {
1118
1088
  if (dev) throw err;
1119
1089
  const key = String(sortExpr);
@@ -1140,12 +1110,27 @@ function safeParseJSON(text) {
1140
1110
  return void 0;
1141
1111
  }
1142
1112
  }
1143
- const isRefinement = (f) => f && typeof f === "object" && f.refine === true;
1144
1113
  function parentPageOf(page, pages) {
1145
1114
  const byRoute = new Map(pages.filter((p) => p?.route).map((p) => [p.route, p]));
1146
1115
  const route = parentRouteOf(page.route, { declared: page.parent ?? null, has: (r) => byRoute.has(r) });
1147
1116
  return route ? byRoute.get(route) : null;
1148
1117
  }
1118
+ function layoutSectionsFor(content, page) {
1119
+ const sets = content?.layouts && typeof content.layouts === "object" ? content.layouts : {};
1120
+ const name = typeof page?.layout?.name === "string" && page.layout.name ? page.layout.name : null;
1121
+ const named = name ? findLayoutEntry(sets, name) : void 0;
1122
+ const chosen = name ? named ? [named] : [] : Object.values(sets);
1123
+ const out = [];
1124
+ const walk = (sections) => {
1125
+ for (const s of sections || []) {
1126
+ if (!s || typeof s !== "object") continue;
1127
+ out.push(s);
1128
+ walk(s.subsections);
1129
+ }
1130
+ };
1131
+ for (const set of chosen) for (const area of Object.values(set || {})) walk(area?.sections);
1132
+ return out;
1133
+ }
1149
1134
  function resolvePageFetchConfigs(content, route, { locale = null } = {}) {
1150
1135
  const { page, params } = findPageForRoute(content, route);
1151
1136
  if (!page) return [];
@@ -1160,7 +1145,15 @@ function resolvePageFetchConfigs(content, route, { locale = null } = {}) {
1160
1145
  variables: binding?.variables ?? null
1161
1146
  };
1162
1147
  const siteFetch = content?.config?.fetch ?? null;
1163
- const routeKey = binding ? routeQuery({ page: page.fetch, parent: parent?.fetch, site: siteFetch, sections: sectionFetches(page.sections) })?.key ?? null : null;
1148
+ const chosen = binding ? pageRouteQuery(page, {
1149
+ routeOf: (p) => p.route,
1150
+ parentOf: (p) => parentPageOf(p, pages),
1151
+ fetchOf: (p) => p.fetch ?? null,
1152
+ sectionsOf: (p) => p.sections,
1153
+ site: siteFetch
1154
+ }) : null;
1155
+ const routeKey = chosen?.key ?? null;
1156
+ const routeSource = chosen?.nested ? chosen.config : null;
1164
1157
  const out = /* @__PURE__ */ new Map();
1165
1158
  const put = (cfg) => {
1166
1159
  const key = deriveCacheKey$1(cfg);
@@ -1168,24 +1161,35 @@ function resolvePageFetchConfigs(content, route, { locale = null } = {}) {
1168
1161
  };
1169
1162
  const add = (sources) => {
1170
1163
  for (const cfg of resolveFetchConfigs(sources, options).values()) {
1171
- put(cfg);
1172
- if (routeKey && cfg.as === routeKey && cfg.detail && binding.paramValue !== void 0) {
1164
+ const current = routeKey && cfg.as === routeKey ? currentOf(cfg) : null;
1165
+ if (current === "exclude") {
1166
+ put(othersView(cfg));
1167
+ continue;
1168
+ }
1169
+ if (current !== "only") {
1170
+ put(cfg);
1171
+ continue;
1172
+ }
1173
+ if (cfg.detail && binding.paramValue !== void 0) {
1173
1174
  const detailCfg = buildDetailConfig(cfg, { paramName: binding.paramName, paramValue: String(binding.paramValue) });
1174
1175
  if (detailCfg) put(detailCfg);
1175
1176
  }
1177
+ put(cfg.ask ? cfg : routeSelection(cfg));
1176
1178
  }
1177
1179
  };
1178
- add([page.fetch ?? null, parent?.fetch ?? null, siteFetch]);
1180
+ const siteForPage = siteReaches(parent) ? siteFetch : null;
1181
+ add([page.fetch ?? null, parent?.fetch ?? null, routeSource, siteForPage]);
1179
1182
  const walk = (sections) => {
1180
1183
  for (const s of sections || []) {
1181
- if (s?.fetch && !isRefinement(s.fetch)) add([s.fetch, page.fetch ?? null, parent?.fetch ?? null, siteFetch]);
1184
+ if (s?.fetch) add([s.fetch, page.fetch ?? null, parent?.fetch ?? null, routeSource, siteForPage]);
1182
1185
  if (s?.subsections) walk(s.subsections);
1183
1186
  }
1184
1187
  };
1185
1188
  walk(page.sections);
1189
+ for (const s of layoutSectionsFor(content, page)) add([s.fetch ?? null, siteFetch]);
1186
1190
  return [...out.values()];
1187
1191
  }
1188
- async function executeFetchConfigs(configs, { content, fetch: fetch2 = null, dev = false, prerender = "always" } = {}) {
1192
+ async function executeFetchConfigs(configs, { content, fetch: fetch2 = null, dev = false, prerender = "always", locale = null } = {}) {
1189
1193
  if (prerender !== "author" && prerender !== "always") {
1190
1194
  throw new Error(`executeFetchConfigs: prerender must be 'author' or 'always', got ${JSON.stringify(prerender)}`);
1191
1195
  }
@@ -1194,7 +1198,7 @@ async function executeFetchConfigs(configs, { content, fetch: fetch2 = null, dev
1194
1198
  dev,
1195
1199
  fetch: fetch2
1196
1200
  });
1197
- const ctx = { website: null };
1201
+ const ctx = { website: null, locale: locale ?? content?.config?.activeLocale ?? resolveDefaultLocale$1(content?.config) ?? null };
1198
1202
  return Promise.all((configs || []).filter(Boolean).map(async (config) => {
1199
1203
  if (prerender === "author" && config.prerender === false) {
1200
1204
  return { config, outcome: "skipped", data: null };
@@ -1206,7 +1210,7 @@ async function executeFetchConfigs(configs, { content, fetch: fetch2 = null, dev
1206
1210
  }
1207
1211
  async function prefetchPageData({ content, route, locale = null, fetch: fetch2 = null, dev = false, prerender = "always" }) {
1208
1212
  const configs = resolvePageFetchConfigs(content, route, { locale });
1209
- return executeFetchConfigs(configs, { content, fetch: fetch2, dev, prerender });
1213
+ return executeFetchConfigs(configs, { content, fetch: fetch2, dev, prerender, locale });
1210
1214
  }
1211
1215
  function createPageRenderer({ website, shell }) {
1212
1216
  if (!website) throw new Error("createPageRenderer: `website` is required");