@uniweb/runtime 1.3.2 → 1.3.3

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
@@ -5,17 +5,18 @@ import { sectionDomId } from "@uniweb/core/section-id";
5
5
  import { applyBasePath } from "@uniweb/core/base-path";
6
6
  import { isDynamicRoute, routePatternToRegex } from "@uniweb/core/route-match";
7
7
  import { findPageForRoute } from "@uniweb/core/route-match";
8
- import { DEFAULT_ICON_BASE, iconUrl } from "@uniweb/core/icon-corpus";
8
+ import { DEFAULT_ICON_BASE, iconUrl, ICON_FAMILY_MAP } from "@uniweb/core/icon-corpus";
9
9
  import { buildTheme, FONT_LINKS_MARKER, buildSectionOverrides } from "@uniweb/theming";
10
10
  import "@uniweb/core/services";
11
11
  import "@uniweb/core/tracker";
12
12
  import { escapeHtml } from "@uniweb/core/page-shell";
13
13
  import { escapeHtml as escapeHtml2, foundationCssUrl, pageShell } from "@uniweb/core/page-shell";
14
+ import { jsonForScript } from "@uniweb/core/data-script";
15
+ import { dataScript, jsonForScript as jsonForScript2 } from "@uniweb/core/data-script";
14
16
  import DataStore, { deriveCacheKey as deriveCacheKey$1 } from "@uniweb/core/datastore";
15
17
  import { RESOLUTION_STATUS, carryQuery, localeOfPath, localeUrl, resolveRoute, routeIndex } from "@uniweb/core/resolve-route";
16
18
  import { resolveFetchConfigs } from "@uniweb/core/fetch-config";
17
19
  import { resolveRecordsService } from "@uniweb/core/records-service";
18
- import { dataScript, jsonForScript } from "@uniweb/core/data-script";
19
20
  function guaranteeItemStructure(item) {
20
21
  return {
21
22
  title: item.title || "",
@@ -800,6 +801,11 @@ ${themeData.links}
800
801
  ${options.sectionOverrideCSS}
801
802
  </style>`;
802
803
  result = result.replace("</head>", `${overrideStyle}
804
+ </head>`);
805
+ }
806
+ if (options.icons?.size && !result.includes('id="__ICON_CACHE__"')) {
807
+ const iconScript = `<script id="__ICON_CACHE__" type="application/json">${jsonForScript(Object.fromEntries(options.icons))}<\/script>`;
808
+ result = result.replace("</head>", () => ` ${iconScript}
803
809
  </head>`);
804
810
  }
805
811
  result = result.replace(
@@ -1191,7 +1197,74 @@ function blocksOf(page) {
1191
1197
  for (const area of Object.values(page.getLayoutAreas?.() ?? {})) walk(area);
1192
1198
  return out;
1193
1199
  }
1194
- async function loadPageData({ website, route, fetch: fetch2, dev = false, locale = null, prerender = "always", cache = null }) {
1200
+ const ICON_WAIT_MS = 1e3;
1201
+ const MAX_ICON_REQUESTS = 40;
1202
+ function parseIconReference(ref) {
1203
+ if (typeof ref !== "string") return null;
1204
+ const colon = ref.indexOf(":");
1205
+ if (colon > 0) return { library: ref.slice(0, colon), name: ref.slice(colon + 1) };
1206
+ const dash = ref.indexOf("-");
1207
+ if (dash < 2 || dash > 3) return null;
1208
+ const library = ref.slice(0, dash);
1209
+ return ICON_FAMILY_MAP[library] ? { library, name: ref.slice(dash + 1) } : null;
1210
+ }
1211
+ function visit(node, found) {
1212
+ if (!node || typeof node !== "object") return;
1213
+ if (node.type === "image" && node.attrs?.role === "icon") found.push(node.attrs);
1214
+ for (const mark of node.marks || []) {
1215
+ const ref = parseIconReference(mark?.attrs?.icon);
1216
+ if (ref) found.push(ref);
1217
+ }
1218
+ if (Array.isArray(node.content)) for (const child of node.content) visit(child, found);
1219
+ }
1220
+ function pageIcons(blocks) {
1221
+ const named = [];
1222
+ for (const block of blocks) visit(block.rawContent?.doc ?? block.rawContent, named);
1223
+ const icons = /* @__PURE__ */ new Map();
1224
+ for (const { library, name } of named) {
1225
+ const family = ICON_FAMILY_MAP[String(library).toLowerCase()];
1226
+ if (family && name) icons.set(`${library}:${name}`, { family, name });
1227
+ }
1228
+ return icons;
1229
+ }
1230
+ async function loadPageIcons({ blocks, website, fetch: fetch2, cache }) {
1231
+ const config = website.config?.icons;
1232
+ if (config?.cdn === false) return;
1233
+ const base = config?.base || DEFAULT_ICON_BASE;
1234
+ const wanted = [...pageIcons(blocks)].filter(([key]) => !cache.has(key)).slice(0, MAX_ICON_REQUESTS);
1235
+ if (wanted.length === 0) return;
1236
+ const all = Promise.all(wanted.map(async ([key, { family, name }]) => {
1237
+ try {
1238
+ const response = await fetch2(iconUrl(family, name, base), { method: "GET" });
1239
+ if (!response.ok) return;
1240
+ const svg = await response.text();
1241
+ if (/<svg[\s>]/i.test(svg)) cache.set(key, svg);
1242
+ } catch {
1243
+ }
1244
+ }));
1245
+ let timer;
1246
+ const waited = new Promise((resolve) => {
1247
+ timer = setTimeout(resolve, ICON_WAIT_MS);
1248
+ });
1249
+ await Promise.race([all, waited]);
1250
+ clearTimeout(timer);
1251
+ }
1252
+ function whileReadingIcons(uniweb, read, draw) {
1253
+ const cache = uniweb?.iconCache;
1254
+ if (!cache) return draw();
1255
+ const { get } = cache;
1256
+ cache.get = (key) => {
1257
+ const svg = get.call(cache, key);
1258
+ if (svg) read.set(key, svg);
1259
+ return svg;
1260
+ };
1261
+ try {
1262
+ return draw();
1263
+ } finally {
1264
+ delete cache.get;
1265
+ }
1266
+ }
1267
+ async function loadPageData({ website, route, fetch: fetch2, dev = false, locale = null, prerender = "always", cache = null, icons = true }) {
1195
1268
  if (prerender !== "author" && prerender !== "always") {
1196
1269
  throw new Error(`loadPageData: prerender must be 'author' or 'always', got ${JSON.stringify(prerender)}`);
1197
1270
  }
@@ -1203,6 +1276,7 @@ async function loadPageData({ website, route, fetch: fetch2, dev = false, locale
1203
1276
  );
1204
1277
  }
1205
1278
  website.makeCurrent();
1279
+ const iconCache = globalThis.uniweb?.iconCache;
1206
1280
  const named = typeof route === "string" ? resolveRequestedPage(website, route) : route;
1207
1281
  const resolved = named ?? (typeof route === "string" ? website.getNotFoundPage() : null);
1208
1282
  if (!resolved) return [];
@@ -1241,11 +1315,13 @@ async function loadPageData({ website, route, fetch: fetch2, dev = false, locale
1241
1315
  }
1242
1316
  };
1243
1317
  const store = website.entityStore;
1318
+ const blocks = blocksOf(page);
1244
1319
  await Promise.all([
1245
1320
  // The page's own record: asked whether or not a section declares the route key, because the
1246
1321
  // page names itself from it and a URL naming no record is a 404.
1247
1322
  store.fetchPageRecord(page, { dispatcher: recording }),
1248
- ...blocksOf(page).map((block) => store.fetch(block, getComponentMeta(block.type), { dispatcher: recording }))
1323
+ ...blocks.map((block) => store.fetch(block, getComponentMeta(block.type), { dispatcher: recording })),
1324
+ icons && iconCache && loadPageIcons({ blocks, website, fetch: fetch2, cache: iconCache })
1249
1325
  ]);
1250
1326
  return [...entries.values()];
1251
1327
  }
@@ -1268,16 +1344,18 @@ function createPageRenderer({ website, shell }) {
1268
1344
  const missing = !named || page?.notFound === true;
1269
1345
  restoreStartPage(website);
1270
1346
  if (typeof target === "string") website.setActivePage(target);
1347
+ const icons = /* @__PURE__ */ new Map();
1271
1348
  let result;
1272
1349
  try {
1273
- result = renderPage(page, website);
1350
+ result = whileReadingIcons(globalThis.uniweb, icons, () => renderPage(page, website));
1274
1351
  } catch (err) {
1275
1352
  return { outcome: missing ? "notFound" : "failed", html: null, page, error: classifyRenderError(err) };
1276
1353
  }
1277
1354
  if (result.error) return { outcome: missing ? "notFound" : "failed", html: null, page, error: result.error };
1278
1355
  const html = injectPageContent(shell, result.renderedContent, page, {
1279
1356
  ...inject,
1280
- sectionOverrideCSS: result.sectionOverrideCSS
1357
+ sectionOverrideCSS: result.sectionOverrideCSS,
1358
+ icons
1281
1359
  });
1282
1360
  return { outcome: missing ? "notFound" : "rendered", html, page, error: null };
1283
1361
  }
@@ -1344,7 +1422,7 @@ export {
1344
1422
  initPrerender,
1345
1423
  initPrerenderForLocale,
1346
1424
  injectPageContent,
1347
- jsonForScript,
1425
+ jsonForScript2 as jsonForScript,
1348
1426
  loadPageData,
1349
1427
  localeOfPath,
1350
1428
  localeUrl,