@uniweb/runtime 0.14.2 → 0.15.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,15 @@
1
- import { isRichSchema, deriveCacheKey, resolveDefaultLocale, hasDarkScheme, createUniweb, resolveRequestStyle, resolveServiceUrl, substitutePlaceholders, matchWhere } from "@uniweb/core";
1
+ import { isRichSchema, deriveCacheKey, resolveDefaultLocale, hasDarkScheme, createUniweb, resolveServiceUrl, substitutePlaceholders, 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
- import { routePatternToRegex } from "@uniweb/core/route-match";
5
+ import { routePatternToRegex, decodeRouteValue, splitPathCapture } from "@uniweb/core/route-match";
6
6
  import { DEFAULT_ICON_BASE, iconUrl } from "@uniweb/core/icon-corpus";
7
7
  import { buildTheme, FONT_LINKS_MARKER, buildSectionOverrides } from "@uniweb/theming";
8
8
  import "@uniweb/core/services";
9
9
  import "@uniweb/core/tracker";
10
10
  import { resolveFetchConfigs } from "@uniweb/core/fetch-config";
11
11
  import { deriveCacheKey as deriveCacheKey$1 } from "@uniweb/core/datastore";
12
+ import { buildDetailConfig } from "@uniweb/core/detail-url";
12
13
  import { resolveDefaultLocale as resolveDefaultLocale$1 } from "@uniweb/core/locale-config";
13
14
  function guaranteeItemStructure(item) {
14
15
  return {
@@ -285,7 +286,7 @@ function hydrateDataStore(website, fetchedData) {
285
286
  if (!website?.dataStore || !fetchedData?.length) return;
286
287
  for (const entry of fetchedData) {
287
288
  if (entry.outcome && entry.outcome !== "fetched") continue;
288
- website.dataStore.set(deriveCacheKey(entry.config), { data: entry.data });
289
+ website.dataStore.set(deriveCacheKey(entry.config), entry.meta ? { data: entry.data, meta: entry.meta } : { data: entry.data });
289
290
  }
290
291
  }
291
292
  function ensureThemeCss(uniweb, foundation) {
@@ -838,48 +839,40 @@ function generate404Html({ baseHtml, website, siteContent }) {
838
839
  }
839
840
  return { html, hasNotFoundPage: !!notFoundPage };
840
841
  }
841
- const KNOWN_OPERATORS = /* @__PURE__ */ new Set(["where", "limit", "sort"]);
842
- function createDefaultFetcher({ basePath = "", config = {}, dev = false, records = null, fetch: fetchImpl = null } = {}) {
842
+ function createDefaultFetcher({ basePath = "", dev = false, records = null, fetch: fetchImpl = null } = {}) {
843
843
  const doFetch = (input, init) => (fetchImpl || globalThis.fetch)(input, init);
844
844
  const pathPrefix = basePath && basePath !== "/" ? basePath.replace(/\/$/, "") : "";
845
- const baseUrl = typeof config?.baseUrl === "string" ? config.baseUrl.replace(/\/$/, "") : "";
846
- const staticHeaders = buildStaticHeaders(config?.headers);
847
- const supports = normalizeSupports(config?.supports);
848
- const requestConfig = config?.request && typeof config.request === "object" ? config.request : {};
849
- const styleName = typeof requestConfig.style === "string" ? requestConfig.style : null;
850
- const style = resolveRequestStyle(styleName, { dev });
851
- const rename = normalizeRename(requestConfig.rename, style, { dev });
852
- const siteEnvelope = config?.envelope && typeof config.envelope === "object" ? config.envelope : null;
853
- const envelope = { ...style.defaultEnvelope || {}, ...siteEnvelope || {} };
854
845
  const stampedArrayKey = records?.envelope && typeof records.envelope === "object" && typeof records.envelope.records === "string" && records.envelope.records.length ? records.envelope.records : null;
855
846
  const laneEnvelope = stampedArrayKey ? { list: stampedArrayKey } : null;
847
+ const doorQueues = /* @__PURE__ */ new Map();
848
+ const askDoor = (request, ctx) => new Promise((resolve) => {
849
+ const url = resolveServiceUrl(request.door, pathPrefix);
850
+ let queue = doorQueues.get(url);
851
+ if (!queue) {
852
+ queue = [];
853
+ doorQueues.set(url, queue);
854
+ queueMicrotask(() => {
855
+ doorQueues.delete(url);
856
+ flushDoor(url, queue, doFetch);
857
+ });
858
+ }
859
+ queue.push({ request, ctx, resolve });
860
+ });
856
861
  return {
857
862
  /**
858
- * Cache-key function. The default-fetcher's cache key includes only
859
- * the operators it pushes down (because they affect what the source
860
- * sees). Operators applied as runtime fallback operate on a shared
861
- * cached value and therefore must NOT split the cache.
862
- *
863
- * Example: with `supports: []`, two pages declaring different
864
- * `where:` clauses against the same path share one cache entry —
865
- * the file is fetched once and each page filters its own copy. With
866
- * `supports: [where]`, the same two pages fire two requests because
867
- * the predicate travels in the request.
863
+ * The cache identity is the request's ADDRESS or, on a question door,
864
+ * the QUESTION (`deriveCacheKey` hashes every operator of an address-less
865
+ * request). Operators evaluated here run over a shared cached value and
866
+ * must NOT split the cache: two pages declaring different `where:` clauses
867
+ * against the same path share one entry — the file is fetched once and
868
+ * each page filters its own copy.
868
869
  */
869
870
  cacheKey(request) {
870
- const base = deriveCacheKey(request);
871
- const projected = {};
872
- for (const op of supports) {
873
- if (!style.canPush.has(op)) continue;
874
- if (request[op] !== void 0) projected[op] = request[op];
875
- }
876
- if (Object.keys(projected).length === 0 && style.name === "json-body") {
877
- return base;
878
- }
879
- return base + "::style=" + style.name + "::" + JSON.stringify(projected);
871
+ return deriveCacheKey(request);
880
872
  },
881
873
  async resolve(request, ctx = {}) {
882
874
  if (!request) return { data: null };
875
+ if (request.door) return askDoor(request, ctx);
883
876
  const { path, url, endpoint, transform, body: rawBody } = request;
884
877
  let method = (request.method || "GET").toUpperCase();
885
878
  if (method !== "GET" && method !== "POST") {
@@ -887,59 +880,39 @@ function createDefaultFetcher({ basePath = "", config = {}, dev = false, records
887
880
  method = "GET";
888
881
  }
889
882
  let target;
890
- let isRemote;
891
883
  if (endpoint) {
892
884
  target = resolveServiceUrl(endpoint, pathPrefix);
893
- isRemote = true;
885
+ if (typeof request.locale === "string" && request.locale) {
886
+ target += (target.includes("?") ? "&" : "?") + "locale=" + encodeURIComponent(request.locale);
887
+ }
894
888
  } else if (path) {
895
889
  target = pathPrefix && path.startsWith("/") && !path.startsWith("//") ? pathPrefix + path : path;
896
- isRemote = false;
897
890
  } else if (url) {
898
- target = isAbsoluteUrl(url) ? url : joinUrl(baseUrl, url);
899
- isRemote = true;
891
+ target = url;
900
892
  } else {
901
893
  return { data: [], error: "No path, url or endpoint specified" };
902
894
  }
903
895
  const init = { signal: ctx.signal, method };
904
- const headers = {};
905
- if (isRemote && staticHeaders) Object.assign(headers, staticHeaders);
906
- const pushCandidates = /* @__PURE__ */ new Set();
907
- if (isRemote) {
908
- for (const op of KNOWN_OPERATORS) {
909
- if (supports.has(op) && style.canPush.has(op) && request[op] !== void 0 && request[op] !== null) {
910
- pushCandidates.add(op);
911
- }
912
- }
913
- }
914
- const encoded = pushCandidates.size > 0 ? style.encode(request, { method, pushCandidates, rename }) : { queryParams: [], bodyMerge: null, pushed: /* @__PURE__ */ new Set() };
915
- const pushedOperators = encoded.pushed;
916
- if (encoded.queryParams.length > 0 && method === "GET") {
917
- target = appendStyleQueryParams(target, encoded.queryParams);
918
- }
919
896
  if (method === "POST") {
920
897
  const dc = request.dynamicContext;
921
- const resolvedBody = rawBody !== void 0 && rawBody !== null && dc && dc.paramName ? substitutePlaceholders(rawBody, { [dc.paramName]: dc.paramValue }, { encode: false }) : rawBody;
922
- const finalBody = composePostBody(resolvedBody, encoded.bodyMerge);
923
- if (finalBody !== null) {
924
- if (!hasHeader(headers, "Content-Type")) {
925
- headers["Content-Type"] = "application/json";
926
- }
927
- init.body = typeof finalBody === "string" ? finalBody : JSON.stringify(finalBody);
898
+ const body = rawBody !== void 0 && rawBody !== null && dc && dc.paramName ? substitutePlaceholders(rawBody, { [dc.paramName]: dc.paramValue }, { encode: false }) : rawBody;
899
+ if (body !== void 0 && body !== null) {
900
+ init.headers = { "Content-Type": "application/json" };
901
+ init.body = typeof body === "string" ? body : JSON.stringify(body);
928
902
  }
929
903
  }
930
- if (Object.keys(headers).length) init.headers = headers;
931
904
  try {
932
905
  const response = await doFetch(target, init);
933
906
  const requestEnvelope = request.envelope && typeof request.envelope === "object" ? request.envelope : null;
934
- const effectiveEnvelope = requestEnvelope ?? (endpoint && laneEnvelope ? { ...envelope, ...laneEnvelope } : envelope);
907
+ const envelope = requestEnvelope ?? (endpoint && laneEnvelope ? laneEnvelope : {});
935
908
  if (!response.ok) {
936
909
  let extracted;
937
- if (effectiveEnvelope.error) {
910
+ if (envelope.error) {
938
911
  try {
939
912
  const text = await response.text();
940
913
  const body = safeParseJSON(text);
941
914
  if (body !== void 0) {
942
- const candidate = getNestedValue(body, effectiveEnvelope.error);
915
+ const candidate = getNestedValue(body, envelope.error);
943
916
  if (typeof candidate === "string" && candidate.length) {
944
917
  extracted = candidate;
945
918
  }
@@ -965,12 +938,13 @@ function createDefaultFetcher({ basePath = "", config = {}, dev = false, records
965
938
  }
966
939
  }
967
940
  const isDetailRequest = !!request.dynamicContext;
968
- const effectiveTransform = transform || (isDetailRequest ? effectiveEnvelope.item : effectiveEnvelope.list);
941
+ const effectiveTransform = transform || (isDetailRequest ? envelope.item : envelope.list);
969
942
  if (effectiveTransform && data !== null && data !== void 0) {
970
943
  data = getNestedValue(data, effectiveTransform);
971
944
  }
972
- data = applyFallbackOperators(data, request, pushedOperators);
973
- return { data: data ?? [] };
945
+ data = applyOperators(data, request, { dev });
946
+ const depth = request.depth === "brief" || request.depth === "full" ? request.depth : void 0;
947
+ return depth ? { data: data ?? [], meta: { depth } } : { data: data ?? [] };
974
948
  } catch (error) {
975
949
  if (error?.name === "AbortError") {
976
950
  return { data: [], error: "aborted" };
@@ -980,105 +954,100 @@ function createDefaultFetcher({ basePath = "", config = {}, dev = false, records
980
954
  }
981
955
  };
982
956
  }
983
- function normalizeRename(raw, style, { dev }) {
984
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
985
- const out = {};
986
- for (const [op, wireName] of Object.entries(raw)) {
987
- if (typeof wireName !== "string" || wireName.length === 0) continue;
988
- if (dev && !style.canPush.has(op) && !warnedRenameTargets.has(op)) {
989
- warnedRenameTargets.add(op);
990
- console.warn(
991
- `[default-fetcher] request.rename: operator "${op}" is not pushed by style "${style.name}" — rename has no effect. Known operators for this style: ${[...style.canPush].join(", ") || "(none)"}.`
992
- );
993
- }
994
- out[op] = wireName;
995
- }
996
- return Object.keys(out).length ? out : null;
957
+ function doorQuestion(request) {
958
+ const q = { schema: request.schema };
959
+ let where = request.where && typeof request.where === "object" ? request.where : null;
960
+ let scope = typeof request.scope === "string" && request.scope ? request.scope : null;
961
+ if (where && !scope && where.path && typeof where.path === "object" && typeof where.path.under === "string" && where.path.under) {
962
+ const { path, ...rest } = where;
963
+ scope = path.under;
964
+ where = Object.keys(rest).length ? rest : null;
965
+ }
966
+ if (scope) q.scope = scope;
967
+ if (where) q.where = renameOperators(where);
968
+ const sort = sortToWire(request.sort);
969
+ if (sort) q.sort = sort;
970
+ if (typeof request.limit === "number" && request.limit > 0) q.limit = request.limit;
971
+ if (request.depth === "brief" || request.depth === "full") q.depth = request.depth;
972
+ return q;
997
973
  }
998
- const warnedRenameTargets = /* @__PURE__ */ new Set();
999
- function normalizeSupports(raw) {
1000
- const out = /* @__PURE__ */ new Set();
1001
- if (!Array.isArray(raw)) return out;
1002
- for (const op of raw) {
1003
- if (typeof op !== "string") continue;
1004
- if (KNOWN_OPERATORS.has(op)) out.add(op);
1005
- else if (!warnedUnknownOperators.has(op)) {
1006
- warnedUnknownOperators.add(op);
1007
- console.warn(`[default-fetcher] supports: unknown operator "${op}" — ignored.`);
1008
- }
974
+ const DOOR_OPERATOR = { nin: "not_in" };
975
+ function renameOperators(where) {
976
+ if (Array.isArray(where)) return where.map(renameOperators);
977
+ if (!where || typeof where !== "object") return where;
978
+ const out = {};
979
+ for (const [key, value] of Object.entries(where)) {
980
+ out[DOOR_OPERATOR[key] ?? key] = value && typeof value === "object" ? renameOperators(value) : value;
1009
981
  }
1010
982
  return out;
1011
983
  }
1012
- const warnedUnknownOperators = /* @__PURE__ */ new Set();
1013
- function appendStyleQueryParams(url, pairs) {
1014
- if (!pairs || pairs.length === 0) return url;
1015
- const params = pairs.map(
1016
- ([k, v]) => encodeURIComponent(k) + "=" + encodeURIComponent(v)
1017
- );
1018
- const sep = url.includes("?") ? "&" : "?";
1019
- return url + sep + params.join("&");
1020
- }
1021
- function composePostBody(authorBody, bodyMerge) {
1022
- if (!bodyMerge) {
1023
- return authorBody === void 0 ? null : authorBody;
1024
- }
1025
- if (typeof authorBody === "string") {
1026
- return authorBody;
1027
- }
1028
- const base = authorBody && typeof authorBody === "object" ? authorBody : {};
1029
- return { ...base, ...bodyMerge };
984
+ async function flushDoor(url, queue, doFetch) {
985
+ const body = {};
986
+ const keys = [];
987
+ for (const entry of queue) {
988
+ const base = entry.request.as || "q";
989
+ let key = base;
990
+ for (let n = 2; key in body; n += 1) key = `${base}#${n}`;
991
+ keys.push(key);
992
+ body[key] = doorQuestion(entry.request);
993
+ }
994
+ let parsed;
995
+ try {
996
+ const response = await doFetch(url, {
997
+ method: "POST",
998
+ headers: { "Content-Type": "application/json" },
999
+ body: JSON.stringify(body)
1000
+ });
1001
+ if (!response.ok) {
1002
+ const error = `HTTP ${response.status}: ${response.statusText}`;
1003
+ for (const entry of queue) entry.resolve({ data: null, error });
1004
+ return;
1005
+ }
1006
+ parsed = await response.json();
1007
+ } catch (error) {
1008
+ const message = error?.name === "AbortError" ? "aborted" : error?.message || String(error);
1009
+ for (const entry of queue) entry.resolve({ data: null, error: message });
1010
+ return;
1011
+ }
1012
+ const data = parsed && typeof parsed.data === "object" && parsed.data ? parsed.data : {};
1013
+ const errors = parsed && typeof parsed.errors === "object" && parsed.errors ? parsed.errors : {};
1014
+ const depths = parsed && typeof parsed.depths === "object" && parsed.depths ? parsed.depths : {};
1015
+ queue.forEach((entry, i) => {
1016
+ const key = keys[i];
1017
+ if (key in errors) {
1018
+ const e = errors[key];
1019
+ entry.resolve({ data: null, error: typeof e === "string" ? e : e?.message || JSON.stringify(e) });
1020
+ return;
1021
+ }
1022
+ if (!(key in data)) {
1023
+ entry.resolve({ data: null, error: `the records door answered without the key "${key}"` });
1024
+ return;
1025
+ }
1026
+ const depth = depths[key] === "brief" || depths[key] === "full" ? depths[key] : entry.request.depth === "brief" || entry.request.depth === "full" ? entry.request.depth : void 0;
1027
+ entry.resolve(depth ? { data: data[key], meta: { depth } } : { data: data[key] });
1028
+ });
1030
1029
  }
1031
- function applyFallbackOperators(data, request, pushedOperators) {
1030
+ function applyOperators(data, request, { dev = false } = {}) {
1032
1031
  if (!Array.isArray(data)) return data;
1033
1032
  let result = data;
1034
- if (request.where && !pushedOperators.has("where")) {
1035
- result = matchWhere(request.where, result);
1036
- }
1037
- if (request.sort && !pushedOperators.has("sort")) {
1038
- result = applySortFallback(result, request.sort);
1039
- }
1040
- if (typeof request.limit === "number" && request.limit > 0 && !pushedOperators.has("limit")) {
1041
- result = result.slice(0, request.limit);
1042
- }
1033
+ if (request.where) result = matchWhere(request.where, result);
1034
+ if (request.sort) result = applySort(result, request.sort, dev);
1035
+ if (typeof request.limit === "number" && request.limit > 0) result = result.slice(0, request.limit);
1043
1036
  return result;
1044
1037
  }
1045
- function applySortFallback(items, sortExpr) {
1046
- const sorts = String(sortExpr).split(",").map((s) => {
1047
- const [field, dir = "asc"] = s.trim().split(/\s+/);
1048
- return { field, desc: dir.toLowerCase() === "desc" };
1049
- });
1050
- return [...items].sort((a, b) => {
1051
- for (const { field, desc } of sorts) {
1052
- const av = getNestedValue(a, field) ?? "";
1053
- const bv = getNestedValue(b, field) ?? "";
1054
- if (av < bv) return desc ? 1 : -1;
1055
- if (av > bv) return desc ? -1 : 1;
1038
+ const warnedBadSorts = /* @__PURE__ */ new Set();
1039
+ function applySort(items, sortExpr, dev) {
1040
+ try {
1041
+ return sortRecords(items, sortExpr);
1042
+ } catch (err) {
1043
+ if (dev) throw err;
1044
+ const key = String(sortExpr);
1045
+ if (!warnedBadSorts.has(key)) {
1046
+ warnedBadSorts.add(key);
1047
+ console.error(`[default-fetcher] ${err.message} Records delivered unsorted.`);
1056
1048
  }
1057
- return 0;
1058
- });
1059
- }
1060
- function buildStaticHeaders(headers) {
1061
- if (!headers || typeof headers !== "object" || Array.isArray(headers)) return null;
1062
- const out = {};
1063
- for (const [k, v] of Object.entries(headers)) {
1064
- if (v === null || v === void 0) continue;
1065
- out[k] = String(v);
1049
+ return items;
1066
1050
  }
1067
- return Object.keys(out).length ? out : null;
1068
- }
1069
- function hasHeader(headers, name) {
1070
- const lower = name.toLowerCase();
1071
- return Object.keys(headers).some((k) => k.toLowerCase() === lower);
1072
- }
1073
- function isAbsoluteUrl(url) {
1074
- if (typeof url !== "string") return false;
1075
- if (url.startsWith("//")) return true;
1076
- return /^[a-z][a-z0-9+.-]*:\/\//i.test(url);
1077
- }
1078
- function joinUrl(baseUrl, url) {
1079
- if (!baseUrl) return url;
1080
- if (url.startsWith("/")) return baseUrl + url;
1081
- return baseUrl + "/" + url;
1082
1051
  }
1083
1052
  function getNestedValue(obj, path) {
1084
1053
  if (!obj || !path) return obj;
@@ -1105,20 +1074,39 @@ function findPageForRoute(content, route) {
1105
1074
  if (!page.isDynamic || !page.route) continue;
1106
1075
  const compiled = routePatternToRegex(page.route);
1107
1076
  const m = compiled?.regex ? compiled.regex.exec(route) : null;
1108
- if (m) return { page, params: Object.fromEntries((compiled.paramNames || []).map((n, i) => [n, m[i + 1]])) };
1077
+ if (m) {
1078
+ const params = {};
1079
+ (compiled.paramNames || []).forEach((n, i) => {
1080
+ const raw = m[i + 1];
1081
+ params[n] = n === compiled.catchAll ? raw.split("/").map(decodeRouteValue).join("/") : decodeRouteValue(raw);
1082
+ });
1083
+ return { page, params };
1084
+ }
1109
1085
  }
1110
1086
  return { page: null, params: {} };
1111
1087
  }
1088
+ function routeBinding(page, params) {
1089
+ const { catchAll } = routePatternToRegex(page.route);
1090
+ if (catchAll && params[catchAll] !== void 0) {
1091
+ const parts = splitPathCapture(params[catchAll]);
1092
+ const paramName2 = page.paramName || "slug";
1093
+ return { paramName: paramName2, paramValue: parts.slug, variables: { ...params, ...parts } };
1094
+ }
1095
+ const paramName = page.paramName || Object.keys(params)[0];
1096
+ return { paramName, paramValue: params[paramName], variables: { ...params } };
1097
+ }
1112
1098
  function resolvePageFetchConfigs(content, route, { locale = null } = {}) {
1113
- const { page } = findPageForRoute(content, route);
1099
+ const { page, params } = findPageForRoute(content, route);
1114
1100
  if (!page) return [];
1115
1101
  const pages = content?.pages || [];
1116
1102
  const parent = page.parent ? pages.find((p) => p.route === page.parent) : null;
1103
+ const binding = page.isDynamic && Object.keys(params).length ? routeBinding(page, params) : null;
1117
1104
  const options = {
1118
1105
  locale,
1119
1106
  defaultLocale: resolveDefaultLocale$1(content?.config) ?? null,
1120
1107
  queries: content?.config?.queries ?? null,
1121
- records: content?.config?.records ?? null
1108
+ records: content?.config?.records ?? null,
1109
+ variables: binding?.variables ?? null
1122
1110
  };
1123
1111
  const out = /* @__PURE__ */ new Map();
1124
1112
  const add = (sources) => {
@@ -1135,6 +1123,14 @@ function resolvePageFetchConfigs(content, route, { locale = null } = {}) {
1135
1123
  }
1136
1124
  };
1137
1125
  walk(page.sections);
1126
+ if (binding && binding.paramValue !== void 0 && page.parentSchema) {
1127
+ const listCfg = [...out.values()].find((cfg) => cfg.as === page.parentSchema && cfg.detail);
1128
+ const detailCfg = listCfg ? buildDetailConfig(listCfg, { paramName: binding.paramName, paramValue: String(binding.paramValue) }) : null;
1129
+ if (detailCfg) {
1130
+ const key = deriveCacheKey$1(detailCfg);
1131
+ if (!out.has(key)) out.set(key, detailCfg);
1132
+ }
1133
+ }
1138
1134
  return [...out.values()];
1139
1135
  }
1140
1136
  async function executeFetchConfigs(configs, { content, fetch: fetch2 = null, dev = false, prerender = "always" } = {}) {
@@ -1143,24 +1139,19 @@ async function executeFetchConfigs(configs, { content, fetch: fetch2 = null, dev
1143
1139
  }
1144
1140
  const fetcher = createDefaultFetcher({
1145
1141
  basePath: content?.config?.base || "",
1146
- config: content?.config?.fetcher ?? {},
1147
1142
  records: content?.config?.records ?? null,
1148
1143
  dev,
1149
1144
  fetch: fetch2
1150
1145
  });
1151
1146
  const ctx = { website: null };
1152
- const out = [];
1153
- for (const config of configs || []) {
1154
- if (!config) continue;
1147
+ return Promise.all((configs || []).filter(Boolean).map(async (config) => {
1155
1148
  if (prerender === "author" && config.prerender === false) {
1156
- out.push({ config, outcome: "skipped", data: null });
1157
- continue;
1149
+ return { config, outcome: "skipped", data: null };
1158
1150
  }
1159
1151
  const result = await fetcher.resolve(config, ctx);
1160
- if (result?.error) out.push({ config, outcome: "failed", data: null, error: result.error });
1161
- else out.push({ config, outcome: "fetched", data: result?.data ?? null });
1162
- }
1163
- return out;
1152
+ if (result?.error) return { config, outcome: "failed", data: null, error: result.error };
1153
+ return { config, outcome: "fetched", data: result?.data ?? null, ...result?.meta ? { meta: result.meta } : {} };
1154
+ }));
1164
1155
  }
1165
1156
  async function prefetchPageData({ content, route, locale = null, fetch: fetch2 = null, dev = false, prerender = "always" }) {
1166
1157
  const configs = resolvePageFetchConfigs(content, route, { locale });