@uniweb/runtime 0.19.4 → 0.20.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
@@ -1,14 +1,14 @@
1
- import { isRichSchema, deriveCacheKey, resolveDefaultLocale, hasDarkScheme, createUniweb, substitutePlaceholders, resolveServiceUrl, matchWhere, sortRecords, sortToWire } from "@uniweb/core";
1
+ import { isRichSchema, deriveCacheKey, resolveDefaultLocale, hasDarkScheme, createUniweb, substitutePlaceholders, resolveServiceUrl, applyScope, matchWhere, sortRecords, sortToWire } from "@uniweb/core";
2
2
  import React from "react";
3
3
  import { renderToString } from "react-dom/server";
4
4
  import { sectionDomId } from "@uniweb/core/section-id";
5
5
  import { applyBasePath } from "@uniweb/core/base-path";
6
- import { routePatternToRegex, decodeRouteValue, splitPathCapture } from "@uniweb/core/route-match";
6
+ import { routePatternToRegex, isDynamicRoute, decodeRouteValue, routeBinding, parentRouteOf } from "@uniweb/core/route-match";
7
7
  import { DEFAULT_ICON_BASE, iconUrl } from "@uniweb/core/icon-corpus";
8
8
  import { buildTheme, FONT_LINKS_MARKER, buildSectionOverrides } from "@uniweb/theming";
9
9
  import "@uniweb/core/services";
10
10
  import "@uniweb/core/tracker";
11
- import { resolveFetchConfigs } from "@uniweb/core/fetch-config";
11
+ import { routeQuery, sectionFetches, resolveFetchConfigs } from "@uniweb/core/fetch-config";
12
12
  import { deriveCacheKey as deriveCacheKey$1 } from "@uniweb/core/datastore";
13
13
  import { buildDetailConfig } from "@uniweb/core/detail-url";
14
14
  import { resolveDefaultLocale as resolveDefaultLocale$1 } from "@uniweb/core/locale-config";
@@ -861,14 +861,22 @@ function createDefaultFetcher({ basePath = "", dev = false, fetch: fetchImpl = n
861
861
  queue.push({ request, ctx, resolve });
862
862
  });
863
863
  };
864
+ const reads = /* @__PURE__ */ new Map();
865
+ const readOnce = (target, init) => {
866
+ const key = `${init.method} ${target} ${init.body ?? ""}`;
867
+ let pending = reads.get(key);
868
+ if (!pending) {
869
+ pending = readResponse(doFetch, target, init).finally(() => reads.delete(key));
870
+ reads.set(key, pending);
871
+ }
872
+ return pending;
873
+ };
864
874
  return {
865
875
  /**
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
868
- * request). Operators evaluated here run over a shared cached value and
869
- * must NOT split the cache: two pages declaring different `where:` clauses
870
- * against the same path share one entry — the file is fetched once and
871
- * each page filters its own copy.
876
+ * The cache identity is the request's ADDRESS and the view it takes of it —
877
+ * or, when asked of the records service, the QUESTION (`deriveCacheKey`).
878
+ * Two pages declaring different `where:` clauses against one path are two
879
+ * entries, each cut from a read of the file (`readOnce`, above).
872
880
  */
873
881
  cacheKey(request) {
874
882
  return deriveCacheKey(request);
@@ -890,7 +898,7 @@ function createDefaultFetcher({ basePath = "", dev = false, fetch: fetchImpl = n
890
898
  } else {
891
899
  return { data: [], error: "No path, url or ask specified" };
892
900
  }
893
- const init = { signal: ctx.signal, method };
901
+ const init = { method };
894
902
  if (method === "POST") {
895
903
  const dc = request.dynamicContext;
896
904
  const body = rawBody !== void 0 && rawBody !== null && dc && dc.paramName ? substitutePlaceholders(rawBody, { [dc.paramName]: dc.paramValue }, { encode: false }) : rawBody;
@@ -900,21 +908,18 @@ function createDefaultFetcher({ basePath = "", dev = false, fetch: fetchImpl = n
900
908
  }
901
909
  }
902
910
  try {
903
- const response = await doFetch(target, init);
911
+ const response = await readOnce(target, init);
912
+ if (ctx.signal?.aborted) return { data: [], error: "aborted" };
904
913
  const envelope = request.envelope && typeof request.envelope === "object" ? request.envelope : {};
905
914
  if (!response.ok) {
906
915
  let extracted;
907
- if (envelope.error) {
908
- try {
909
- const text = await response.text();
910
- const body = safeParseJSON(text);
911
- if (body !== void 0) {
912
- const candidate = getNestedValue(body, envelope.error);
913
- if (typeof candidate === "string" && candidate.length) {
914
- extracted = candidate;
915
- }
916
+ if (envelope.error && typeof response.text === "string") {
917
+ const body = safeParseJSON(response.text);
918
+ if (body !== void 0) {
919
+ const candidate = getNestedValue(body, envelope.error);
920
+ if (typeof candidate === "string" && candidate.length) {
921
+ extracted = candidate;
916
922
  }
917
- } catch {
918
923
  }
919
924
  }
920
925
  return {
@@ -922,18 +927,7 @@ function createDefaultFetcher({ basePath = "", dev = false, fetch: fetchImpl = n
922
927
  error: extracted ?? `HTTP ${response.status}: ${response.statusText}`
923
928
  };
924
929
  }
925
- const contentType = response.headers.get("content-type") || "";
926
- let data;
927
- if (contentType.includes("application/json")) {
928
- data = await response.json();
929
- } else {
930
- const text = await response.text();
931
- try {
932
- data = JSON.parse(text);
933
- } catch {
934
- data = text;
935
- }
936
- }
930
+ let data = response.body;
937
931
  const isDetailRequest = !!request.dynamicContext;
938
932
  const effectiveTransform = transform || (isDetailRequest ? envelope.item : envelope.list);
939
933
  if (effectiveTransform && data !== null && data !== void 0) {
@@ -953,15 +947,11 @@ function createDefaultFetcher({ basePath = "", dev = false, fetch: fetchImpl = n
953
947
  }
954
948
  function toQuestion(request) {
955
949
  const q = { schema: request.schema };
956
- let where = request.where && typeof request.where === "object" ? request.where : null;
957
- let scope = typeof request.scope === "string" && request.scope ? request.scope : null;
958
- if (where && !scope && where.path && typeof where.path === "object" && typeof where.path.under === "string" && where.path.under) {
959
- const { path, ...rest } = where;
960
- scope = path.under;
961
- where = Object.keys(rest).length ? rest : null;
962
- }
950
+ const where = request.where && typeof request.where === "object" ? request.where : null;
951
+ const scope = typeof request.scope === "string" && request.scope ? request.scope : null;
963
952
  if (scope) q.scope = scope;
964
953
  if (where) q.where = renameOperators(where);
954
+ if (request.match && typeof request.match === "object") q.match = request.match;
965
955
  const sort = sortToWire(request.sort);
966
956
  if (sort) q.sort = sort;
967
957
  if (typeof request.limit === "number" && request.limit > 0) q.limit = request.limit;
@@ -1086,6 +1076,25 @@ async function flushAsked(url, queue, doFetch) {
1086
1076
  return flushAsked(url, [next], doFetch);
1087
1077
  }));
1088
1078
  }
1079
+ async function readResponse(doFetch, target, init) {
1080
+ const response = await doFetch(target, init);
1081
+ if (!response.ok) {
1082
+ let text2 = null;
1083
+ try {
1084
+ text2 = await response.text();
1085
+ } catch {
1086
+ }
1087
+ return { ok: false, status: response.status, statusText: response.statusText, text: text2 };
1088
+ }
1089
+ const contentType = response.headers.get("content-type") || "";
1090
+ if (contentType.includes("application/json")) return { ok: true, body: await response.json() };
1091
+ const text = await response.text();
1092
+ try {
1093
+ return { ok: true, body: JSON.parse(text) };
1094
+ } catch {
1095
+ return { ok: true, body: text };
1096
+ }
1097
+ }
1089
1098
  function withMeta(fields) {
1090
1099
  const out = {};
1091
1100
  for (const [k, v] of Object.entries(fields)) if (v !== void 0) out[k] = v;
@@ -1094,6 +1103,7 @@ function withMeta(fields) {
1094
1103
  function applyOperators(data, request, { dev = false } = {}) {
1095
1104
  if (!Array.isArray(data)) return data;
1096
1105
  let result = data;
1106
+ if (typeof request.scope === "string" && request.scope) result = applyScope(result, request.scope);
1097
1107
  if (request.where) result = matchWhere(request.where, result);
1098
1108
  if (request.sort) result = applySort(result, request.sort, dev);
1099
1109
  if (typeof request.limit === "number" && request.limit > 0) result = result.slice(0, request.limit);
@@ -1135,7 +1145,7 @@ function findPageForRoute(content, route) {
1135
1145
  const exact = pages.find((p) => p.route === route);
1136
1146
  if (exact) return { page: exact, params: {} };
1137
1147
  for (const page of pages) {
1138
- if (!page.isDynamic || !page.route) continue;
1148
+ if (!page.route || !isDynamicRoute(page.route)) continue;
1139
1149
  const compiled = routePatternToRegex(page.route);
1140
1150
  const m = compiled?.regex ? compiled.regex.exec(route) : null;
1141
1151
  if (m) {
@@ -1149,22 +1159,17 @@ function findPageForRoute(content, route) {
1149
1159
  }
1150
1160
  return { page: null, params: {} };
1151
1161
  }
1152
- function routeBinding(page, params) {
1153
- const { catchAll } = routePatternToRegex(page.route);
1154
- if (catchAll && params[catchAll] !== void 0) {
1155
- const parts = splitPathCapture(params[catchAll]);
1156
- const paramName2 = page.paramName || "slug";
1157
- return { paramName: paramName2, paramValue: parts.slug, variables: { ...params, ...parts } };
1158
- }
1159
- const paramName = page.paramName || Object.keys(params)[0];
1160
- return { paramName, paramValue: params[paramName], variables: { ...params } };
1162
+ function parentPageOf(page, pages) {
1163
+ const byRoute = new Map(pages.filter((p) => p?.route).map((p) => [p.route, p]));
1164
+ const route = parentRouteOf(page.route, { declared: page.parent ?? null, has: (r) => byRoute.has(r) });
1165
+ return route ? byRoute.get(route) : null;
1161
1166
  }
1162
1167
  function resolvePageFetchConfigs(content, route, { locale = null } = {}) {
1163
1168
  const { page, params } = findPageForRoute(content, route);
1164
1169
  if (!page) return [];
1165
1170
  const pages = content?.pages || [];
1166
- const parent = page.parent ? pages.find((p) => p.route === page.parent) : null;
1167
- const binding = page.isDynamic && Object.keys(params).length ? routeBinding(page, params) : null;
1171
+ const parent = parentPageOf(page, pages);
1172
+ const binding = isDynamicRoute(page.route) && Object.keys(params).length ? routeBinding(page.route, params, page.paramName ?? null) : null;
1168
1173
  const options = {
1169
1174
  locale,
1170
1175
  defaultLocale: resolveDefaultLocale$1(content?.config) ?? null,
@@ -1172,29 +1177,30 @@ function resolvePageFetchConfigs(content, route, { locale = null } = {}) {
1172
1177
  services: content?.config?.services ?? null,
1173
1178
  variables: binding?.variables ?? null
1174
1179
  };
1180
+ const siteFetch = content?.config?.fetch ?? null;
1181
+ const routeKey = binding ? routeQuery({ page: page.fetch, parent: parent?.fetch, site: siteFetch, sections: sectionFetches(page.sections) })?.key ?? null : null;
1175
1182
  const out = /* @__PURE__ */ new Map();
1183
+ const put = (cfg) => {
1184
+ const key = deriveCacheKey$1(cfg);
1185
+ if (!out.has(key)) out.set(key, cfg);
1186
+ };
1176
1187
  const add = (sources) => {
1177
1188
  for (const cfg of resolveFetchConfigs(sources, options).values()) {
1178
- const key = deriveCacheKey$1(cfg);
1179
- if (!out.has(key)) out.set(key, cfg);
1189
+ put(cfg);
1190
+ if (routeKey && cfg.as === routeKey && cfg.detail && binding.paramValue !== void 0) {
1191
+ const detailCfg = buildDetailConfig(cfg, { paramName: binding.paramName, paramValue: String(binding.paramValue) });
1192
+ if (detailCfg) put(detailCfg);
1193
+ }
1180
1194
  }
1181
1195
  };
1182
- add([page.fetch ?? null, parent?.fetch ?? null, content?.config?.fetch ?? null]);
1196
+ add([page.fetch ?? null, parent?.fetch ?? null, siteFetch]);
1183
1197
  const walk = (sections) => {
1184
1198
  for (const s of sections || []) {
1185
- if (s?.fetch && !isRefinement(s.fetch)) add([s.fetch, page.fetch ?? null, parent?.fetch ?? null, content?.config?.fetch ?? null]);
1199
+ if (s?.fetch && !isRefinement(s.fetch)) add([s.fetch, page.fetch ?? null, parent?.fetch ?? null, siteFetch]);
1186
1200
  if (s?.subsections) walk(s.subsections);
1187
1201
  }
1188
1202
  };
1189
1203
  walk(page.sections);
1190
- if (binding && binding.paramValue !== void 0 && page.parentSchema) {
1191
- const listCfg = [...out.values()].find((cfg) => cfg.as === page.parentSchema && cfg.detail);
1192
- const detailCfg = listCfg ? buildDetailConfig(listCfg, { paramName: binding.paramName, paramValue: String(binding.paramValue) }) : null;
1193
- if (detailCfg) {
1194
- const key = deriveCacheKey$1(detailCfg);
1195
- if (!out.has(key)) out.set(key, detailCfg);
1196
- }
1197
- }
1198
1204
  return [...out.values()];
1199
1205
  }
1200
1206
  async function executeFetchConfigs(configs, { content, fetch: fetch2 = null, dev = false, prerender = "always" } = {}) {