@uniweb/runtime 0.14.1 → 0.14.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 +51 -192
- package/dist/ssr.js.map +1 -1
- package/package.json +2 -2
- package/src/page-renderer.js +158 -0
- package/src/prefetch.js +8 -0
- package/src/ssr.js +6 -0
package/dist/ssr.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import { isRichSchema, deriveCacheKey
|
|
1
|
+
import { isRichSchema, deriveCacheKey, resolveDefaultLocale, hasDarkScheme, createUniweb, resolveRequestStyle, resolveServiceUrl, substitutePlaceholders, matchWhere } 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";
|
|
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 { resolveDefaultLocale as resolveDefaultLocale$1 } from "@uniweb/core/locale-config";
|
|
6
13
|
function guaranteeItemStructure(item) {
|
|
7
14
|
return {
|
|
8
15
|
title: item.title || "",
|
|
@@ -240,27 +247,6 @@ function getComponentMeta(componentName) {
|
|
|
240
247
|
function getComponentDefaults(componentName) {
|
|
241
248
|
return globalThis.uniweb?.getComponentDefaults?.(componentName) || {};
|
|
242
249
|
}
|
|
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
250
|
function default404Html(basePath = "") {
|
|
265
251
|
const homeHref = basePath ? `${basePath}/` : "/";
|
|
266
252
|
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 +268,7 @@ function wireFoundationCapabilities(uniweb, foundation) {
|
|
|
282
268
|
}
|
|
283
269
|
}
|
|
284
270
|
function sliceContentForLocale(content, locale) {
|
|
285
|
-
const defaultLang = resolveDefaultLocale
|
|
271
|
+
const defaultLang = resolveDefaultLocale(content?.config);
|
|
286
272
|
const locData = content?.locales?.[locale];
|
|
287
273
|
if (!locale || locale === defaultLang || !locData) return content;
|
|
288
274
|
return {
|
|
@@ -299,7 +285,7 @@ function hydrateDataStore(website, fetchedData) {
|
|
|
299
285
|
if (!website?.dataStore || !fetchedData?.length) return;
|
|
300
286
|
for (const entry of fetchedData) {
|
|
301
287
|
if (entry.outcome && entry.outcome !== "fetched") continue;
|
|
302
|
-
website.dataStore.set(deriveCacheKey
|
|
288
|
+
website.dataStore.set(deriveCacheKey(entry.config), { data: entry.data });
|
|
303
289
|
}
|
|
304
290
|
}
|
|
305
291
|
function ensureThemeCss(uniweb, foundation) {
|
|
@@ -628,7 +614,7 @@ function renderLayout(page, website) {
|
|
|
628
614
|
function initPrerenderForLocale(content, foundation, locale, extensionsOrOptions, maybeOptions) {
|
|
629
615
|
const localeContent = sliceContentForLocale(content, locale);
|
|
630
616
|
const uniweb = initPrerender(localeContent, foundation, extensionsOrOptions, maybeOptions);
|
|
631
|
-
const defaultLang = resolveDefaultLocale
|
|
617
|
+
const defaultLang = resolveDefaultLocale(content?.config);
|
|
632
618
|
if (locale && locale !== defaultLang && uniweb.activeWebsite?.setActiveLocale) {
|
|
633
619
|
uniweb.activeWebsite.setActiveLocale(locale);
|
|
634
620
|
}
|
|
@@ -852,169 +838,6 @@ function generate404Html({ baseHtml, website, siteContent }) {
|
|
|
852
838
|
}
|
|
853
839
|
return { html, hasNotFoundPage: !!notFoundPage };
|
|
854
840
|
}
|
|
855
|
-
const DATA_DIR = "data";
|
|
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
841
|
const KNOWN_OPERATORS = /* @__PURE__ */ new Set(["where", "limit", "sort"]);
|
|
1019
842
|
function createDefaultFetcher({ basePath = "", config = {}, dev = false, records = null, fetch: fetchImpl = null } = {}) {
|
|
1020
843
|
const doFetch = (input, init) => (fetchImpl || globalThis.fetch)(input, init);
|
|
@@ -1044,7 +867,7 @@ function createDefaultFetcher({ basePath = "", config = {}, dev = false, records
|
|
|
1044
867
|
* the predicate travels in the request.
|
|
1045
868
|
*/
|
|
1046
869
|
cacheKey(request) {
|
|
1047
|
-
const base = deriveCacheKey
|
|
870
|
+
const base = deriveCacheKey(request);
|
|
1048
871
|
const projected = {};
|
|
1049
872
|
for (const op of supports) {
|
|
1050
873
|
if (!style.canPush.has(op)) continue;
|
|
@@ -1095,7 +918,7 @@ function createDefaultFetcher({ basePath = "", config = {}, dev = false, records
|
|
|
1095
918
|
}
|
|
1096
919
|
if (method === "POST") {
|
|
1097
920
|
const dc = request.dynamicContext;
|
|
1098
|
-
const resolvedBody = rawBody !== void 0 && rawBody !== null && dc && dc.paramName ? substitutePlaceholders
|
|
921
|
+
const resolvedBody = rawBody !== void 0 && rawBody !== null && dc && dc.paramName ? substitutePlaceholders(rawBody, { [dc.paramName]: dc.paramValue }, { encode: false }) : rawBody;
|
|
1099
922
|
const finalBody = composePostBody(resolvedBody, encoded.bodyMerge);
|
|
1100
923
|
if (finalBody !== null) {
|
|
1101
924
|
if (!hasHeader(headers, "Content-Type")) {
|
|
@@ -1293,14 +1116,14 @@ function resolvePageFetchConfigs(content, route, { locale = null } = {}) {
|
|
|
1293
1116
|
const parent = page.parent ? pages.find((p) => p.route === page.parent) : null;
|
|
1294
1117
|
const options = {
|
|
1295
1118
|
locale,
|
|
1296
|
-
defaultLocale: resolveDefaultLocale(content?.config) ?? null,
|
|
1119
|
+
defaultLocale: resolveDefaultLocale$1(content?.config) ?? null,
|
|
1297
1120
|
queries: content?.config?.queries ?? null,
|
|
1298
1121
|
records: content?.config?.records ?? null
|
|
1299
1122
|
};
|
|
1300
1123
|
const out = /* @__PURE__ */ new Map();
|
|
1301
1124
|
const add = (sources) => {
|
|
1302
1125
|
for (const cfg of resolveFetchConfigs(sources, options).values()) {
|
|
1303
|
-
const key = deriveCacheKey(cfg);
|
|
1126
|
+
const key = deriveCacheKey$1(cfg);
|
|
1304
1127
|
if (!out.has(key)) out.set(key, cfg);
|
|
1305
1128
|
}
|
|
1306
1129
|
};
|
|
@@ -1343,10 +1166,45 @@ async function prefetchPageData({ content, route, locale = null, fetch: fetch2 =
|
|
|
1343
1166
|
const configs = resolvePageFetchConfigs(content, route, { locale });
|
|
1344
1167
|
return executeFetchConfigs(configs, { content, fetch: fetch2, dev, prerender });
|
|
1345
1168
|
}
|
|
1169
|
+
function createPageRenderer({ website, shell }) {
|
|
1170
|
+
if (!website) throw new Error("createPageRenderer: `website` is required");
|
|
1171
|
+
if (typeof shell !== "string") throw new Error("createPageRenderer: `shell` must be an HTML string");
|
|
1172
|
+
function render(target, { inject = {} } = {}) {
|
|
1173
|
+
const page = typeof target === "string" ? resolvePage(website, target) : target;
|
|
1174
|
+
if (!page) return { outcome: "notFound", html: null, page: null, error: null };
|
|
1175
|
+
let result;
|
|
1176
|
+
try {
|
|
1177
|
+
result = renderPage(page, website);
|
|
1178
|
+
} catch (err) {
|
|
1179
|
+
return { outcome: "failed", html: null, page, error: classifyRenderError(err) };
|
|
1180
|
+
}
|
|
1181
|
+
if (result.error) return { outcome: "failed", html: null, page, error: result.error };
|
|
1182
|
+
const html = injectPageContent(shell, result.renderedContent, page, {
|
|
1183
|
+
...inject,
|
|
1184
|
+
sectionOverrideCSS: result.sectionOverrideCSS
|
|
1185
|
+
});
|
|
1186
|
+
return { outcome: "rendered", html, page, error: null };
|
|
1187
|
+
}
|
|
1188
|
+
return { website, render };
|
|
1189
|
+
}
|
|
1190
|
+
async function prefetchAndHydrate({ website, content, route, locale = null, fetch: fetch2 = null, dev = false, prerender = "always" }) {
|
|
1191
|
+
if (!website?.dataStore) {
|
|
1192
|
+
throw new Error("prefetchAndHydrate: `website` must be an initialized Website with a dataStore");
|
|
1193
|
+
}
|
|
1194
|
+
if (typeof fetch2 !== "function") {
|
|
1195
|
+
throw new Error(
|
|
1196
|
+
"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`."
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
const fetched = await prefetchPageData({ content, route, locale, fetch: fetch2, dev, prerender });
|
|
1200
|
+
hydrateDataStore(website, fetched);
|
|
1201
|
+
return fetched;
|
|
1202
|
+
}
|
|
1346
1203
|
export {
|
|
1347
1204
|
applyDefaults,
|
|
1348
1205
|
applySchemas,
|
|
1349
1206
|
classifyRenderError,
|
|
1207
|
+
createPageRenderer,
|
|
1350
1208
|
escapeHtml,
|
|
1351
1209
|
executeFetchConfigs,
|
|
1352
1210
|
findPageForRoute,
|
|
@@ -1359,6 +1217,7 @@ export {
|
|
|
1359
1217
|
initPrerender,
|
|
1360
1218
|
initPrerenderForLocale,
|
|
1361
1219
|
injectPageContent,
|
|
1220
|
+
prefetchAndHydrate,
|
|
1362
1221
|
prefetchIcons,
|
|
1363
1222
|
prefetchPageData,
|
|
1364
1223
|
prepareProps,
|