@uniweb/runtime 0.14.1 → 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 +206 -356
- package/dist/ssr.js.map +1 -1
- package/package.json +3 -3
- package/src/components/BlockRenderer.jsx +7 -0
- package/src/default-fetcher.js +234 -378
- package/src/isolate-api.js +86 -0
- package/src/page-renderer.js +158 -0
- package/src/prefetch.js +71 -17
- package/src/prepare-props.js +1 -1
- package/src/setup.js +7 -5
- package/src/ssr.js +6 -0
- package/src/wire-foundation.js +3 -1
package/dist/ssr.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import { isRichSchema, deriveCacheKey
|
|
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, decodeRouteValue, splitPathCapture } from "@uniweb/core/route-match";
|
|
6
|
+
import { DEFAULT_ICON_BASE, iconUrl } from "@uniweb/core/icon-corpus";
|
|
5
7
|
import { buildTheme, FONT_LINKS_MARKER, buildSectionOverrides } from "@uniweb/theming";
|
|
8
|
+
import "@uniweb/core/services";
|
|
9
|
+
import "@uniweb/core/tracker";
|
|
10
|
+
import { resolveFetchConfigs } from "@uniweb/core/fetch-config";
|
|
11
|
+
import { deriveCacheKey as deriveCacheKey$1 } from "@uniweb/core/datastore";
|
|
12
|
+
import { buildDetailConfig } from "@uniweb/core/detail-url";
|
|
13
|
+
import { resolveDefaultLocale as resolveDefaultLocale$1 } from "@uniweb/core/locale-config";
|
|
6
14
|
function guaranteeItemStructure(item) {
|
|
7
15
|
return {
|
|
8
16
|
title: item.title || "",
|
|
@@ -240,27 +248,6 @@ function getComponentMeta(componentName) {
|
|
|
240
248
|
function getComponentDefaults(componentName) {
|
|
241
249
|
return globalThis.uniweb?.getComponentDefaults?.(componentName) || {};
|
|
242
250
|
}
|
|
243
|
-
const PARAM_NAME = "[A-Za-z0-9_-]+";
|
|
244
|
-
const REGEX_SPECIALS = /[.*+?^${}()|[\]\\]/g;
|
|
245
|
-
function normalizeRoute(route) {
|
|
246
|
-
if (typeof route !== "string" || route === "") return "/";
|
|
247
|
-
return route === "/" ? "/" : route.replace(/\/+$/, "") || "/";
|
|
248
|
-
}
|
|
249
|
-
function routePatternToRegex(pattern) {
|
|
250
|
-
const paramNames = [];
|
|
251
|
-
const source = normalizeRoute(pattern).replace(REGEX_SPECIALS, "\\$&").replace(new RegExp(`:(${PARAM_NAME})`, "g"), (_, name) => {
|
|
252
|
-
paramNames.push(name);
|
|
253
|
-
return "([^/]+)";
|
|
254
|
-
});
|
|
255
|
-
return { regex: new RegExp(`^${source}$`), paramNames };
|
|
256
|
-
}
|
|
257
|
-
const DEFAULT_ICON_BASE = "https://uniweb.github.io/icons";
|
|
258
|
-
function iconPath(family, name) {
|
|
259
|
-
return `${family}/${family}-${name}.svg`;
|
|
260
|
-
}
|
|
261
|
-
function iconUrl(family, name, base = DEFAULT_ICON_BASE) {
|
|
262
|
-
return `${String(base).replace(/\/+$/, "")}/${iconPath(family, name)}`;
|
|
263
|
-
}
|
|
264
251
|
function default404Html(basePath = "") {
|
|
265
252
|
const homeHref = basePath ? `${basePath}/` : "/";
|
|
266
253
|
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>`;
|
|
@@ -282,7 +269,7 @@ function wireFoundationCapabilities(uniweb, foundation) {
|
|
|
282
269
|
}
|
|
283
270
|
}
|
|
284
271
|
function sliceContentForLocale(content, locale) {
|
|
285
|
-
const defaultLang = resolveDefaultLocale
|
|
272
|
+
const defaultLang = resolveDefaultLocale(content?.config);
|
|
286
273
|
const locData = content?.locales?.[locale];
|
|
287
274
|
if (!locale || locale === defaultLang || !locData) return content;
|
|
288
275
|
return {
|
|
@@ -299,7 +286,7 @@ function hydrateDataStore(website, fetchedData) {
|
|
|
299
286
|
if (!website?.dataStore || !fetchedData?.length) return;
|
|
300
287
|
for (const entry of fetchedData) {
|
|
301
288
|
if (entry.outcome && entry.outcome !== "fetched") continue;
|
|
302
|
-
website.dataStore.set(deriveCacheKey
|
|
289
|
+
website.dataStore.set(deriveCacheKey(entry.config), entry.meta ? { data: entry.data, meta: entry.meta } : { data: entry.data });
|
|
303
290
|
}
|
|
304
291
|
}
|
|
305
292
|
function ensureThemeCss(uniweb, foundation) {
|
|
@@ -628,7 +615,7 @@ function renderLayout(page, website) {
|
|
|
628
615
|
function initPrerenderForLocale(content, foundation, locale, extensionsOrOptions, maybeOptions) {
|
|
629
616
|
const localeContent = sliceContentForLocale(content, locale);
|
|
630
617
|
const uniweb = initPrerender(localeContent, foundation, extensionsOrOptions, maybeOptions);
|
|
631
|
-
const defaultLang = resolveDefaultLocale
|
|
618
|
+
const defaultLang = resolveDefaultLocale(content?.config);
|
|
632
619
|
if (locale && locale !== defaultLang && uniweb.activeWebsite?.setActiveLocale) {
|
|
633
620
|
uniweb.activeWebsite.setActiveLocale(locale);
|
|
634
621
|
}
|
|
@@ -852,211 +839,40 @@ function generate404Html({ baseHtml, website, siteContent }) {
|
|
|
852
839
|
}
|
|
853
840
|
return { html, hasNotFoundPage: !!notFoundPage };
|
|
854
841
|
}
|
|
855
|
-
|
|
856
|
-
const DATA_URL_PREFIX = `/${DATA_DIR}/`;
|
|
857
|
-
function queryDataUrl(name) {
|
|
858
|
-
return `${DATA_URL_PREFIX}${name}.json`;
|
|
859
|
-
}
|
|
860
|
-
function recordDataUrl(query, slug) {
|
|
861
|
-
return `${DATA_URL_PREFIX}${query}/${slug}.json`;
|
|
862
|
-
}
|
|
863
|
-
function isDataUrl(path) {
|
|
864
|
-
return typeof path === "string" && path.startsWith(DATA_URL_PREFIX);
|
|
865
|
-
}
|
|
866
|
-
const PLACEHOLDER_RE = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
867
|
-
function substitutePlaceholders(value, context, options = {}) {
|
|
868
|
-
const { encode = true } = options;
|
|
869
|
-
if (typeof value === "string") {
|
|
870
|
-
return value.replace(PLACEHOLDER_RE, (literal, key) => {
|
|
871
|
-
if (!(key in (context || {}))) return literal;
|
|
872
|
-
const raw = context[key];
|
|
873
|
-
if (raw === void 0 || raw === null) return literal;
|
|
874
|
-
return encode ? encodeURIComponent(String(raw)) : String(raw);
|
|
875
|
-
});
|
|
876
|
-
}
|
|
877
|
-
if (Array.isArray(value)) {
|
|
878
|
-
return value.map((item) => substitutePlaceholders(item, context, options));
|
|
879
|
-
}
|
|
880
|
-
if (value && typeof value === "object") {
|
|
881
|
-
const result = {};
|
|
882
|
-
for (const key of Object.keys(value)) {
|
|
883
|
-
result[key] = substitutePlaceholders(value[key], context, options);
|
|
884
|
-
}
|
|
885
|
-
return result;
|
|
886
|
-
}
|
|
887
|
-
return value;
|
|
888
|
-
}
|
|
889
|
-
const PATH_SLOT = "{path}";
|
|
890
|
-
const PARAM_SLOT = "{param}";
|
|
891
|
-
const warnedPatterns = /* @__PURE__ */ new Set();
|
|
892
|
-
function warnOnce(key, message) {
|
|
893
|
-
if (warnedPatterns.has(key)) return;
|
|
894
|
-
warnedPatterns.add(key);
|
|
895
|
-
console.warn(`[query-address] ${message}`);
|
|
896
|
-
}
|
|
897
|
-
function readPattern(lane, key) {
|
|
898
|
-
if (!lane || typeof lane !== "object" || Array.isArray(lane)) return null;
|
|
899
|
-
const pattern = lane[key];
|
|
900
|
-
return typeof pattern === "string" && pattern.length > 0 ? pattern : null;
|
|
901
|
-
}
|
|
902
|
-
function resolveQueryAddress(query, lane) {
|
|
903
|
-
if (typeof query !== "string" || query.length === 0) return null;
|
|
904
|
-
const pattern = readPattern(lane, "list");
|
|
905
|
-
if (!pattern) return null;
|
|
906
|
-
if (!pattern.includes(PATH_SLOT)) {
|
|
907
|
-
warnOnce(
|
|
908
|
-
`list:${pattern}`,
|
|
909
|
-
`config.records.list carries no ${PATH_SLOT} placeholder, so every query would resolve to the same address. Ignoring it and reading the compiled file instead.`
|
|
910
|
-
);
|
|
911
|
-
return null;
|
|
912
|
-
}
|
|
913
|
-
return substitutePlaceholders(pattern, { path: query });
|
|
914
|
-
}
|
|
915
|
-
function resolveRecordAddressPattern(query, lane) {
|
|
916
|
-
if (typeof query !== "string" || query.length === 0) return null;
|
|
917
|
-
const pattern = readPattern(lane, "record");
|
|
918
|
-
if (!pattern) return null;
|
|
919
|
-
if (!pattern.includes(PARAM_SLOT)) {
|
|
920
|
-
warnOnce(
|
|
921
|
-
`record:${pattern}`,
|
|
922
|
-
`config.records.record carries no ${PARAM_SLOT} placeholder, so every record would resolve to the same address. Ignoring it and reading the per-record file instead.`
|
|
923
|
-
);
|
|
924
|
-
return null;
|
|
925
|
-
}
|
|
926
|
-
return substitutePlaceholders(pattern, { path: query });
|
|
927
|
-
}
|
|
928
|
-
function localizeConfig(cfg, locale, defaultLocale) {
|
|
929
|
-
if (!cfg.path) return cfg;
|
|
930
|
-
if (!locale || locale === defaultLocale) return cfg;
|
|
931
|
-
if (!isDataUrl(cfg.path)) return cfg;
|
|
932
|
-
return { ...cfg, path: `/${locale}${cfg.path}` };
|
|
933
|
-
}
|
|
934
|
-
function applyDeferredDetail(cfg, queries, records) {
|
|
935
|
-
if (cfg.detail !== void 0) return cfg;
|
|
936
|
-
if (cfg.endpoint) {
|
|
937
|
-
const recordPattern = resolveRecordAddressPattern(cfg.query, records);
|
|
938
|
-
if (recordPattern) return { ...cfg, detail: recordPattern };
|
|
939
|
-
}
|
|
940
|
-
const queryName = cfg.query || bindingKey(cfg);
|
|
941
|
-
if (!queryName || !queries) return cfg;
|
|
942
|
-
const collConfig = queries[queryName];
|
|
943
|
-
if (!collConfig || typeof collConfig !== "object") return cfg;
|
|
944
|
-
const deferred = Array.isArray(collConfig.deferred) ? collConfig.deferred : null;
|
|
945
|
-
if (!deferred || deferred.length === 0) return cfg;
|
|
946
|
-
const pattern = typeof collConfig.detailUrl === "string" ? collConfig.detailUrl : recordDataUrl(queryName, "{slug}");
|
|
947
|
-
return { ...cfg, detail: pattern };
|
|
948
|
-
}
|
|
949
|
-
function resolveQuerySource(cfg, records) {
|
|
950
|
-
if (typeof cfg.query !== "string" || cfg.query.length === 0) return cfg;
|
|
951
|
-
const endpoint = resolveQueryAddress(cfg.query, records);
|
|
952
|
-
if (endpoint) {
|
|
953
|
-
const { path, url, ...rest } = cfg;
|
|
954
|
-
return { ...rest, endpoint };
|
|
955
|
-
}
|
|
956
|
-
return { ...cfg, path: queryDataUrl(cfg.query) };
|
|
957
|
-
}
|
|
958
|
-
function bindingKey(cfg) {
|
|
959
|
-
return cfg?.as;
|
|
960
|
-
}
|
|
961
|
-
function resolveFetchConfigs(sources, options = {}) {
|
|
962
|
-
const {
|
|
963
|
-
schemas = [],
|
|
964
|
-
locale = null,
|
|
965
|
-
defaultLocale = null,
|
|
966
|
-
queries = null,
|
|
967
|
-
records = null
|
|
968
|
-
} = options;
|
|
969
|
-
const configs = /* @__PURE__ */ new Map();
|
|
970
|
-
const collectAll = schemas.length === 0;
|
|
971
|
-
for (const source of sources) {
|
|
972
|
-
if (!source) continue;
|
|
973
|
-
const configList = Array.isArray(source) ? source : [source];
|
|
974
|
-
for (const cfg of configList) {
|
|
975
|
-
const key = bindingKey(cfg);
|
|
976
|
-
if (!key) continue;
|
|
977
|
-
if (configs.has(key)) continue;
|
|
978
|
-
if (!collectAll && !schemas.includes(key)) continue;
|
|
979
|
-
const sourced = resolveQuerySource(cfg, records);
|
|
980
|
-
const localized = localizeConfig(sourced, locale, defaultLocale);
|
|
981
|
-
configs.set(key, applyDeferredDetail(localized, queries, records));
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
return configs;
|
|
985
|
-
}
|
|
986
|
-
function deriveCacheKey(request) {
|
|
987
|
-
const { path, url, endpoint, transform } = request || {};
|
|
988
|
-
const as = request?.as;
|
|
989
|
-
const method = request?.method && request.method.toUpperCase() !== "GET" ? request.method.toUpperCase() : void 0;
|
|
990
|
-
const body = method === "POST" ? request?.body : void 0;
|
|
991
|
-
return JSON.stringify({ path, url, endpoint, as, transform, method, body });
|
|
992
|
-
}
|
|
993
|
-
function codeOf(entry) {
|
|
994
|
-
if (typeof entry === "string" && entry.trim() && entry.trim() !== "*") return entry.trim();
|
|
995
|
-
if (entry && typeof entry === "object" && typeof entry.code === "string" && entry.code.trim()) {
|
|
996
|
-
return entry.code.trim();
|
|
997
|
-
}
|
|
998
|
-
return null;
|
|
999
|
-
}
|
|
1000
|
-
function normalizeLanguageList(value) {
|
|
1001
|
-
if (!Array.isArray(value)) return [];
|
|
1002
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1003
|
-
const codes = [];
|
|
1004
|
-
for (const entry of value) {
|
|
1005
|
-
const code = codeOf(entry);
|
|
1006
|
-
if (!code || seen.has(code)) continue;
|
|
1007
|
-
seen.add(code);
|
|
1008
|
-
codes.push(code);
|
|
1009
|
-
}
|
|
1010
|
-
return codes;
|
|
1011
|
-
}
|
|
1012
|
-
function resolveDefaultLocale(config = {}) {
|
|
1013
|
-
if (typeof config?.defaultLanguage === "string" && config.defaultLanguage.trim()) {
|
|
1014
|
-
return config.defaultLanguage.trim();
|
|
1015
|
-
}
|
|
1016
|
-
return normalizeLanguageList(config?.languages)[0] || "en";
|
|
1017
|
-
}
|
|
1018
|
-
const KNOWN_OPERATORS = /* @__PURE__ */ new Set(["where", "limit", "sort"]);
|
|
1019
|
-
function createDefaultFetcher({ basePath = "", config = {}, dev = false, records = null, fetch: fetchImpl = null } = {}) {
|
|
842
|
+
function createDefaultFetcher({ basePath = "", dev = false, records = null, fetch: fetchImpl = null } = {}) {
|
|
1020
843
|
const doFetch = (input, init) => (fetchImpl || globalThis.fetch)(input, init);
|
|
1021
844
|
const pathPrefix = basePath && basePath !== "/" ? basePath.replace(/\/$/, "") : "";
|
|
1022
|
-
const baseUrl = typeof config?.baseUrl === "string" ? config.baseUrl.replace(/\/$/, "") : "";
|
|
1023
|
-
const staticHeaders = buildStaticHeaders(config?.headers);
|
|
1024
|
-
const supports = normalizeSupports(config?.supports);
|
|
1025
|
-
const requestConfig = config?.request && typeof config.request === "object" ? config.request : {};
|
|
1026
|
-
const styleName = typeof requestConfig.style === "string" ? requestConfig.style : null;
|
|
1027
|
-
const style = resolveRequestStyle(styleName, { dev });
|
|
1028
|
-
const rename = normalizeRename(requestConfig.rename, style, { dev });
|
|
1029
|
-
const siteEnvelope = config?.envelope && typeof config.envelope === "object" ? config.envelope : null;
|
|
1030
|
-
const envelope = { ...style.defaultEnvelope || {}, ...siteEnvelope || {} };
|
|
1031
845
|
const stampedArrayKey = records?.envelope && typeof records.envelope === "object" && typeof records.envelope.records === "string" && records.envelope.records.length ? records.envelope.records : null;
|
|
1032
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
|
+
});
|
|
1033
861
|
return {
|
|
1034
862
|
/**
|
|
1035
|
-
*
|
|
1036
|
-
* the
|
|
1037
|
-
*
|
|
1038
|
-
*
|
|
1039
|
-
*
|
|
1040
|
-
*
|
|
1041
|
-
* `where:` clauses against the same path share one cache entry —
|
|
1042
|
-
* the file is fetched once and each page filters its own copy. With
|
|
1043
|
-
* `supports: [where]`, the same two pages fire two requests because
|
|
1044
|
-
* 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.
|
|
1045
869
|
*/
|
|
1046
870
|
cacheKey(request) {
|
|
1047
|
-
|
|
1048
|
-
const projected = {};
|
|
1049
|
-
for (const op of supports) {
|
|
1050
|
-
if (!style.canPush.has(op)) continue;
|
|
1051
|
-
if (request[op] !== void 0) projected[op] = request[op];
|
|
1052
|
-
}
|
|
1053
|
-
if (Object.keys(projected).length === 0 && style.name === "json-body") {
|
|
1054
|
-
return base;
|
|
1055
|
-
}
|
|
1056
|
-
return base + "::style=" + style.name + "::" + JSON.stringify(projected);
|
|
871
|
+
return deriveCacheKey(request);
|
|
1057
872
|
},
|
|
1058
873
|
async resolve(request, ctx = {}) {
|
|
1059
874
|
if (!request) return { data: null };
|
|
875
|
+
if (request.door) return askDoor(request, ctx);
|
|
1060
876
|
const { path, url, endpoint, transform, body: rawBody } = request;
|
|
1061
877
|
let method = (request.method || "GET").toUpperCase();
|
|
1062
878
|
if (method !== "GET" && method !== "POST") {
|
|
@@ -1064,59 +880,39 @@ function createDefaultFetcher({ basePath = "", config = {}, dev = false, records
|
|
|
1064
880
|
method = "GET";
|
|
1065
881
|
}
|
|
1066
882
|
let target;
|
|
1067
|
-
let isRemote;
|
|
1068
883
|
if (endpoint) {
|
|
1069
884
|
target = resolveServiceUrl(endpoint, pathPrefix);
|
|
1070
|
-
|
|
885
|
+
if (typeof request.locale === "string" && request.locale) {
|
|
886
|
+
target += (target.includes("?") ? "&" : "?") + "locale=" + encodeURIComponent(request.locale);
|
|
887
|
+
}
|
|
1071
888
|
} else if (path) {
|
|
1072
889
|
target = pathPrefix && path.startsWith("/") && !path.startsWith("//") ? pathPrefix + path : path;
|
|
1073
|
-
isRemote = false;
|
|
1074
890
|
} else if (url) {
|
|
1075
|
-
target =
|
|
1076
|
-
isRemote = true;
|
|
891
|
+
target = url;
|
|
1077
892
|
} else {
|
|
1078
893
|
return { data: [], error: "No path, url or endpoint specified" };
|
|
1079
894
|
}
|
|
1080
895
|
const init = { signal: ctx.signal, method };
|
|
1081
|
-
const headers = {};
|
|
1082
|
-
if (isRemote && staticHeaders) Object.assign(headers, staticHeaders);
|
|
1083
|
-
const pushCandidates = /* @__PURE__ */ new Set();
|
|
1084
|
-
if (isRemote) {
|
|
1085
|
-
for (const op of KNOWN_OPERATORS) {
|
|
1086
|
-
if (supports.has(op) && style.canPush.has(op) && request[op] !== void 0 && request[op] !== null) {
|
|
1087
|
-
pushCandidates.add(op);
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
const encoded = pushCandidates.size > 0 ? style.encode(request, { method, pushCandidates, rename }) : { queryParams: [], bodyMerge: null, pushed: /* @__PURE__ */ new Set() };
|
|
1092
|
-
const pushedOperators = encoded.pushed;
|
|
1093
|
-
if (encoded.queryParams.length > 0 && method === "GET") {
|
|
1094
|
-
target = appendStyleQueryParams(target, encoded.queryParams);
|
|
1095
|
-
}
|
|
1096
896
|
if (method === "POST") {
|
|
1097
897
|
const dc = request.dynamicContext;
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
headers["Content-Type"] = "application/json";
|
|
1103
|
-
}
|
|
1104
|
-
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);
|
|
1105
902
|
}
|
|
1106
903
|
}
|
|
1107
|
-
if (Object.keys(headers).length) init.headers = headers;
|
|
1108
904
|
try {
|
|
1109
905
|
const response = await doFetch(target, init);
|
|
1110
906
|
const requestEnvelope = request.envelope && typeof request.envelope === "object" ? request.envelope : null;
|
|
1111
|
-
const
|
|
907
|
+
const envelope = requestEnvelope ?? (endpoint && laneEnvelope ? laneEnvelope : {});
|
|
1112
908
|
if (!response.ok) {
|
|
1113
909
|
let extracted;
|
|
1114
|
-
if (
|
|
910
|
+
if (envelope.error) {
|
|
1115
911
|
try {
|
|
1116
912
|
const text = await response.text();
|
|
1117
913
|
const body = safeParseJSON(text);
|
|
1118
914
|
if (body !== void 0) {
|
|
1119
|
-
const candidate = getNestedValue(body,
|
|
915
|
+
const candidate = getNestedValue(body, envelope.error);
|
|
1120
916
|
if (typeof candidate === "string" && candidate.length) {
|
|
1121
917
|
extracted = candidate;
|
|
1122
918
|
}
|
|
@@ -1142,12 +938,13 @@ function createDefaultFetcher({ basePath = "", config = {}, dev = false, records
|
|
|
1142
938
|
}
|
|
1143
939
|
}
|
|
1144
940
|
const isDetailRequest = !!request.dynamicContext;
|
|
1145
|
-
const effectiveTransform = transform || (isDetailRequest ?
|
|
941
|
+
const effectiveTransform = transform || (isDetailRequest ? envelope.item : envelope.list);
|
|
1146
942
|
if (effectiveTransform && data !== null && data !== void 0) {
|
|
1147
943
|
data = getNestedValue(data, effectiveTransform);
|
|
1148
944
|
}
|
|
1149
|
-
data =
|
|
1150
|
-
|
|
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 ?? [] };
|
|
1151
948
|
} catch (error) {
|
|
1152
949
|
if (error?.name === "AbortError") {
|
|
1153
950
|
return { data: [], error: "aborted" };
|
|
@@ -1157,105 +954,100 @@ function createDefaultFetcher({ basePath = "", config = {}, dev = false, records
|
|
|
1157
954
|
}
|
|
1158
955
|
};
|
|
1159
956
|
}
|
|
1160
|
-
function
|
|
1161
|
-
|
|
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;
|
|
973
|
+
}
|
|
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;
|
|
1162
978
|
const out = {};
|
|
1163
|
-
for (const [
|
|
1164
|
-
|
|
1165
|
-
if (dev && !style.canPush.has(op) && !warnedRenameTargets.has(op)) {
|
|
1166
|
-
warnedRenameTargets.add(op);
|
|
1167
|
-
console.warn(
|
|
1168
|
-
`[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)"}.`
|
|
1169
|
-
);
|
|
1170
|
-
}
|
|
1171
|
-
out[op] = wireName;
|
|
1172
|
-
}
|
|
1173
|
-
return Object.keys(out).length ? out : null;
|
|
1174
|
-
}
|
|
1175
|
-
const warnedRenameTargets = /* @__PURE__ */ new Set();
|
|
1176
|
-
function normalizeSupports(raw) {
|
|
1177
|
-
const out = /* @__PURE__ */ new Set();
|
|
1178
|
-
if (!Array.isArray(raw)) return out;
|
|
1179
|
-
for (const op of raw) {
|
|
1180
|
-
if (typeof op !== "string") continue;
|
|
1181
|
-
if (KNOWN_OPERATORS.has(op)) out.add(op);
|
|
1182
|
-
else if (!warnedUnknownOperators.has(op)) {
|
|
1183
|
-
warnedUnknownOperators.add(op);
|
|
1184
|
-
console.warn(`[default-fetcher] supports: unknown operator "${op}" — ignored.`);
|
|
1185
|
-
}
|
|
979
|
+
for (const [key, value] of Object.entries(where)) {
|
|
980
|
+
out[DOOR_OPERATOR[key] ?? key] = value && typeof value === "object" ? renameOperators(value) : value;
|
|
1186
981
|
}
|
|
1187
982
|
return out;
|
|
1188
983
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
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
|
+
});
|
|
1207
1029
|
}
|
|
1208
|
-
function
|
|
1030
|
+
function applyOperators(data, request, { dev = false } = {}) {
|
|
1209
1031
|
if (!Array.isArray(data)) return data;
|
|
1210
1032
|
let result = data;
|
|
1211
|
-
if (request.where
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
if (request.sort && !pushedOperators.has("sort")) {
|
|
1215
|
-
result = applySortFallback(result, request.sort);
|
|
1216
|
-
}
|
|
1217
|
-
if (typeof request.limit === "number" && request.limit > 0 && !pushedOperators.has("limit")) {
|
|
1218
|
-
result = result.slice(0, request.limit);
|
|
1219
|
-
}
|
|
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);
|
|
1220
1036
|
return result;
|
|
1221
1037
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
return
|
|
1226
|
-
})
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
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.`);
|
|
1233
1048
|
}
|
|
1234
|
-
return
|
|
1235
|
-
});
|
|
1236
|
-
}
|
|
1237
|
-
function buildStaticHeaders(headers) {
|
|
1238
|
-
if (!headers || typeof headers !== "object" || Array.isArray(headers)) return null;
|
|
1239
|
-
const out = {};
|
|
1240
|
-
for (const [k, v] of Object.entries(headers)) {
|
|
1241
|
-
if (v === null || v === void 0) continue;
|
|
1242
|
-
out[k] = String(v);
|
|
1049
|
+
return items;
|
|
1243
1050
|
}
|
|
1244
|
-
return Object.keys(out).length ? out : null;
|
|
1245
|
-
}
|
|
1246
|
-
function hasHeader(headers, name) {
|
|
1247
|
-
const lower = name.toLowerCase();
|
|
1248
|
-
return Object.keys(headers).some((k) => k.toLowerCase() === lower);
|
|
1249
|
-
}
|
|
1250
|
-
function isAbsoluteUrl(url) {
|
|
1251
|
-
if (typeof url !== "string") return false;
|
|
1252
|
-
if (url.startsWith("//")) return true;
|
|
1253
|
-
return /^[a-z][a-z0-9+.-]*:\/\//i.test(url);
|
|
1254
|
-
}
|
|
1255
|
-
function joinUrl(baseUrl, url) {
|
|
1256
|
-
if (!baseUrl) return url;
|
|
1257
|
-
if (url.startsWith("/")) return baseUrl + url;
|
|
1258
|
-
return baseUrl + "/" + url;
|
|
1259
1051
|
}
|
|
1260
1052
|
function getNestedValue(obj, path) {
|
|
1261
1053
|
if (!obj || !path) return obj;
|
|
@@ -1282,25 +1074,44 @@ function findPageForRoute(content, route) {
|
|
|
1282
1074
|
if (!page.isDynamic || !page.route) continue;
|
|
1283
1075
|
const compiled = routePatternToRegex(page.route);
|
|
1284
1076
|
const m = compiled?.regex ? compiled.regex.exec(route) : null;
|
|
1285
|
-
if (m)
|
|
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
|
+
}
|
|
1286
1085
|
}
|
|
1287
1086
|
return { page: null, params: {} };
|
|
1288
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
|
+
}
|
|
1289
1098
|
function resolvePageFetchConfigs(content, route, { locale = null } = {}) {
|
|
1290
|
-
const { page } = findPageForRoute(content, route);
|
|
1099
|
+
const { page, params } = findPageForRoute(content, route);
|
|
1291
1100
|
if (!page) return [];
|
|
1292
1101
|
const pages = content?.pages || [];
|
|
1293
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;
|
|
1294
1104
|
const options = {
|
|
1295
1105
|
locale,
|
|
1296
|
-
defaultLocale: resolveDefaultLocale(content?.config) ?? null,
|
|
1106
|
+
defaultLocale: resolveDefaultLocale$1(content?.config) ?? null,
|
|
1297
1107
|
queries: content?.config?.queries ?? null,
|
|
1298
|
-
records: content?.config?.records ?? null
|
|
1108
|
+
records: content?.config?.records ?? null,
|
|
1109
|
+
variables: binding?.variables ?? null
|
|
1299
1110
|
};
|
|
1300
1111
|
const out = /* @__PURE__ */ new Map();
|
|
1301
1112
|
const add = (sources) => {
|
|
1302
1113
|
for (const cfg of resolveFetchConfigs(sources, options).values()) {
|
|
1303
|
-
const key = deriveCacheKey(cfg);
|
|
1114
|
+
const key = deriveCacheKey$1(cfg);
|
|
1304
1115
|
if (!out.has(key)) out.set(key, cfg);
|
|
1305
1116
|
}
|
|
1306
1117
|
};
|
|
@@ -1312,6 +1123,14 @@ function resolvePageFetchConfigs(content, route, { locale = null } = {}) {
|
|
|
1312
1123
|
}
|
|
1313
1124
|
};
|
|
1314
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
|
+
}
|
|
1315
1134
|
return [...out.values()];
|
|
1316
1135
|
}
|
|
1317
1136
|
async function executeFetchConfigs(configs, { content, fetch: fetch2 = null, dev = false, prerender = "always" } = {}) {
|
|
@@ -1320,33 +1139,63 @@ async function executeFetchConfigs(configs, { content, fetch: fetch2 = null, dev
|
|
|
1320
1139
|
}
|
|
1321
1140
|
const fetcher = createDefaultFetcher({
|
|
1322
1141
|
basePath: content?.config?.base || "",
|
|
1323
|
-
config: content?.config?.fetcher ?? {},
|
|
1324
1142
|
records: content?.config?.records ?? null,
|
|
1325
1143
|
dev,
|
|
1326
1144
|
fetch: fetch2
|
|
1327
1145
|
});
|
|
1328
1146
|
const ctx = { website: null };
|
|
1329
|
-
|
|
1330
|
-
for (const config of configs || []) {
|
|
1331
|
-
if (!config) continue;
|
|
1147
|
+
return Promise.all((configs || []).filter(Boolean).map(async (config) => {
|
|
1332
1148
|
if (prerender === "author" && config.prerender === false) {
|
|
1333
|
-
|
|
1334
|
-
continue;
|
|
1149
|
+
return { config, outcome: "skipped", data: null };
|
|
1335
1150
|
}
|
|
1336
1151
|
const result = await fetcher.resolve(config, ctx);
|
|
1337
|
-
if (result?.error)
|
|
1338
|
-
|
|
1339
|
-
}
|
|
1340
|
-
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
|
+
}));
|
|
1341
1155
|
}
|
|
1342
1156
|
async function prefetchPageData({ content, route, locale = null, fetch: fetch2 = null, dev = false, prerender = "always" }) {
|
|
1343
1157
|
const configs = resolvePageFetchConfigs(content, route, { locale });
|
|
1344
1158
|
return executeFetchConfigs(configs, { content, fetch: fetch2, dev, prerender });
|
|
1345
1159
|
}
|
|
1160
|
+
function createPageRenderer({ website, shell }) {
|
|
1161
|
+
if (!website) throw new Error("createPageRenderer: `website` is required");
|
|
1162
|
+
if (typeof shell !== "string") throw new Error("createPageRenderer: `shell` must be an HTML string");
|
|
1163
|
+
function render(target, { inject = {} } = {}) {
|
|
1164
|
+
const page = typeof target === "string" ? resolvePage(website, target) : target;
|
|
1165
|
+
if (!page) return { outcome: "notFound", html: null, page: null, error: null };
|
|
1166
|
+
let result;
|
|
1167
|
+
try {
|
|
1168
|
+
result = renderPage(page, website);
|
|
1169
|
+
} catch (err) {
|
|
1170
|
+
return { outcome: "failed", html: null, page, error: classifyRenderError(err) };
|
|
1171
|
+
}
|
|
1172
|
+
if (result.error) return { outcome: "failed", html: null, page, error: result.error };
|
|
1173
|
+
const html = injectPageContent(shell, result.renderedContent, page, {
|
|
1174
|
+
...inject,
|
|
1175
|
+
sectionOverrideCSS: result.sectionOverrideCSS
|
|
1176
|
+
});
|
|
1177
|
+
return { outcome: "rendered", html, page, error: null };
|
|
1178
|
+
}
|
|
1179
|
+
return { website, render };
|
|
1180
|
+
}
|
|
1181
|
+
async function prefetchAndHydrate({ website, content, route, locale = null, fetch: fetch2 = null, dev = false, prerender = "always" }) {
|
|
1182
|
+
if (!website?.dataStore) {
|
|
1183
|
+
throw new Error("prefetchAndHydrate: `website` must be an initialized Website with a dataStore");
|
|
1184
|
+
}
|
|
1185
|
+
if (typeof fetch2 !== "function") {
|
|
1186
|
+
throw new Error(
|
|
1187
|
+
"prefetchAndHydrate: `fetch` must be a function. A transport does not survive a JSON-serialized isolate boundary — pass it as an RPC method argument. To use the ambient fetch deliberately, pass `fetch: globalThis.fetch`."
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
const fetched = await prefetchPageData({ content, route, locale, fetch: fetch2, dev, prerender });
|
|
1191
|
+
hydrateDataStore(website, fetched);
|
|
1192
|
+
return fetched;
|
|
1193
|
+
}
|
|
1346
1194
|
export {
|
|
1347
1195
|
applyDefaults,
|
|
1348
1196
|
applySchemas,
|
|
1349
1197
|
classifyRenderError,
|
|
1198
|
+
createPageRenderer,
|
|
1350
1199
|
escapeHtml,
|
|
1351
1200
|
executeFetchConfigs,
|
|
1352
1201
|
findPageForRoute,
|
|
@@ -1359,6 +1208,7 @@ export {
|
|
|
1359
1208
|
initPrerender,
|
|
1360
1209
|
initPrerenderForLocale,
|
|
1361
1210
|
injectPageContent,
|
|
1211
|
+
prefetchAndHydrate,
|
|
1362
1212
|
prefetchIcons,
|
|
1363
1213
|
prefetchPageData,
|
|
1364
1214
|
prepareProps,
|