mnfst-render 0.5.37 → 0.5.38

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  /* Manifest Render */
4
4
 
5
- import { readFileSync, readSync, mkdirSync, writeFileSync, existsSync, rmSync, statSync, readdirSync, cpSync, unlinkSync, renameSync } from 'node:fs';
5
+ import { readFileSync, readSync, mkdirSync, writeFileSync, appendFileSync, existsSync, rmSync, statSync, readdirSync, cpSync, unlinkSync, renameSync } from 'node:fs';
6
6
  import { spawnSync } from 'node:child_process';
7
7
  import { join, resolve, dirname, relative, basename, sep } from 'node:path';
8
8
  import { createServer } from 'node:http';
@@ -282,6 +282,10 @@ function resolveConfig() {
282
282
  : [],
283
283
  dryRun: !!cli.dryRun,
284
284
  debugPrerender: !!cli.debugPrerender,
285
+ // render.icons: "lazy" strips prerender-inlined x-icon SVGs from the
286
+ // output — the icons plugin re-renders them at boot (decorative content;
287
+ // an icon-heavy nav duplicates kilobytes of identical SVG per link).
288
+ lazyIcons: pre.icons === 'lazy',
285
289
  // Cap on the manifest:render-ready wait. When the data plugin dispatches
286
290
  // the event, we resolve immediately; when it doesn't (most projects), we
287
291
  // fall back to the timeout. 10s gives slow data plugin pipelines a
@@ -1127,6 +1131,18 @@ function stripRedundantImgSrcBindings(html) {
1127
1131
  */
1128
1132
  // Prerender inlined Iconify SVG under <i x-icon="iterator.icon">; clear x-icon value so Alpine does not evaluate
1129
1133
  // loop/item expressions while the attribute remains for CSS (e.g. inline layout that keys off [x-icon]).
1134
+ // render.icons: "lazy" — drop prerender-inlined icon SVGs from the output.
1135
+ // Each svg carries its resolved name (data-icon), which is written back into
1136
+ // the host's x-icon attribute as a static, scope-free value — so the icons
1137
+ // plugin re-renders it at boot even on baked clones whose expression attrs
1138
+ // were emptied. Only svg-as-first-child hosts match; anything else stays inline.
1139
+ function stripLazyIconSvgs(html) {
1140
+ return html.replace(
1141
+ /(x-icon\s*=\s*")[^"]*("[^>]*>\s*)<svg\b[^>]*\bdata-icon="([^"]+)"[^>]*>[\s\S]*?<\/svg>/gi,
1142
+ (m, pre, post, name) => `${pre}${name}${post}`
1143
+ );
1144
+ }
1145
+
1130
1146
  function stripResolvedXIconDirectives(html) {
1131
1147
  return html.replace(/<i\b([^>]*)>([\s\S]*?)<\/i>/gi, (full, attrs, inner) => {
1132
1148
  if (isHydrateMarkedAttrs(attrs)) return full;
@@ -1593,6 +1609,7 @@ function generateLocaleVariantHtml({
1593
1609
  html = stripRedundantImgSrcBindings(html);
1594
1610
  html = stripEmptyInlineMaskStyles(html);
1595
1611
  html = stripResolvedXIconDirectives(html);
1612
+ if (config.lazyIcons) html = stripLazyIconSvgs(html);
1596
1613
  // markPrerenderedManifestComponents must run BEFORE stripPrerenderHydrateMarkers so it can
1597
1614
  // detect data-hydrate markers and skip components inside hydrate islands.
1598
1615
  html = markPrerenderedManifestComponents(html);
@@ -1874,7 +1891,8 @@ async function waitForVisualSettle(page, { settleMs = 300, mdWaitMs = 5000, capM
1874
1891
  const bootPending = !!window.__manifestReadyCoordinator && !window.__manifestReady;
1875
1892
  const inflight = (window.__manifestMarkdownPending || 0) > 0;
1876
1893
  const mdPending = cur.mdEmpty > 0 && Date.now() - start < mdWaitMs;
1877
- const utilPending = elapsed < mdWaitMs + 4000 && unresolvedVariants() > 0;
1894
+ const utilPending = (window.__manifestUtilitiesPending || 0) > 0
1895
+ || (elapsed < mdWaitMs + 4000 && unresolvedVariants() > 0);
1878
1896
  // A throttled compiler can DROP the request that would resolve them —
1879
1897
  // a class mutation re-triggers its observer's schedule.
1880
1898
  if (utilPending && elapsed % 500 < 100) document.body.classList.toggle('mnfst-og-nudge');
@@ -1991,13 +2009,26 @@ async function takeOgSnapshot(page, outputDir, pathSeg, globalAssetSignature, ca
1991
2009
  // surface (headless pages can sit frames behind after heavy updates).
1992
2010
  await page.screenshot({ type: 'png', clip: { x: 0, y: 0, width: 8, height: 8 }, captureBeyondViewport: false }).catch(() => { });
1993
2011
  await new Promise((r) => setTimeout(r, 120));
1994
- await page.screenshot({
2012
+ const shoot = () => page.screenshot({
1995
2013
  path: filePath,
1996
2014
  type: 'png',
1997
2015
  clip: { x: 0, y: 0, width: 1200, height: 630 },
1998
2016
  omitBackground: false,
1999
2017
  captureBeyondViewport: false,
2000
2018
  });
2019
+ await shoot();
2020
+ // Residual-race retry: a text-heavy page yielding a shell-weight card
2021
+ // means the main content missed the frame — settle again and reshoot.
2022
+ for (let attempt = 0; attempt < 2; attempt++) {
2023
+ let size = 0;
2024
+ try { size = statSync(filePath).size; } catch { break; }
2025
+ if (size >= 34 * 1024) break;
2026
+ const textLen = await page.evaluate(() => document.body?.innerText.length || 0).catch(() => 0);
2027
+ if (textLen < 2500) break;
2028
+ await new Promise((r) => setTimeout(r, 1200));
2029
+ await waitForVisualSettle(page, { settleMs: 400, capMs: 5000 });
2030
+ await shoot();
2031
+ }
2001
2032
  // Sanity check: a blank 1200×630 PNG (header only, white body) is ~8–10KB;
2002
2033
  // a content-rich page is 50KB+. When the resulting file is suspiciously
2003
2034
  // small the snapshot is treated as failed and the renderer falls through
@@ -4447,6 +4478,16 @@ async function runPrerender(config) {
4447
4478
  });
4448
4479
  });
4449
4480
 
4481
+ // (render.icons: "lazy" is applied on the serialized HTML string — see
4482
+ // stripLazyIconSvgs. Doing it in the live DOM re-triggers the icons
4483
+ // plugin's observer, which re-injects every svg before serialization.)
4484
+
4485
+ // x-virtual recomputes spacer geometry at boot; measured heights vary
4486
+ // run to run — zero them so identical content serializes identically.
4487
+ await page.evaluate(() => {
4488
+ document.querySelectorAll('[data-virtual-spacer]').forEach((el) => { el.style.height = '0px'; });
4489
+ });
4490
+
4450
4491
  // Deterministic output: randomized CSS anchor-positioning names
4451
4492
  // (--dropdown-x7f2, --popover-…, --anchor-…) are regenerated per run and
4452
4493
  // made every render a phantom diff. Renumber them sequentially in DOM
@@ -4547,6 +4588,7 @@ async function runPrerender(config) {
4547
4588
  html = stripRedundantImgSrcBindings(html);
4548
4589
  html = stripEmptyInlineMaskStyles(html);
4549
4590
  html = stripResolvedXIconDirectives(html);
4591
+ if (config.lazyIcons) html = stripLazyIconSvgs(html);
4550
4592
  html = markPrerenderedManifestComponents(html);
4551
4593
 
4552
4594
  // Prefix internal <a> links with the locale for non-default locales so
@@ -4790,9 +4832,18 @@ async function runPrerender(config) {
4790
4832
  if (bundleUtilities) {
4791
4833
  const utilMerged = mergeUtilityCssBlocks(utilityBlocks);
4792
4834
  if (utilMerged.trim()) {
4793
- writeFileSync(join(outputResolved, 'prerender.utilities.css'), `${utilMerged}\n`, 'utf8');
4794
- process.stdout.write('prerender: wrote prerender.utilities.css (Manifest custom utilities)\n');
4795
- postProcessInjectStylesheetLink(outputResolved, 'prerender.utilities.css', routerBasePath || '');
4835
+ // Append to prerender.tailwind.css when it exists — one render-blocking
4836
+ // stylesheet instead of two, same cascade order (utilities last). The
4837
+ // separate file remains only for projects without a Tailwind build.
4838
+ const twPath = join(outputResolved, 'prerender.tailwind.css');
4839
+ if (existsSync(twPath)) {
4840
+ appendFileSync(twPath, `\n/* Manifest custom utilities */\n${utilMerged}\n`, 'utf8');
4841
+ process.stdout.write('prerender: merged custom utilities into prerender.tailwind.css\n');
4842
+ } else {
4843
+ writeFileSync(join(outputResolved, 'prerender.utilities.css'), `${utilMerged}\n`, 'utf8');
4844
+ process.stdout.write('prerender: wrote prerender.utilities.css (Manifest custom utilities)\n');
4845
+ postProcessInjectStylesheetLink(outputResolved, 'prerender.utilities.css', routerBasePath || '');
4846
+ }
4796
4847
  }
4797
4848
  }
4798
4849
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-render",
3
- "version": "0.5.37",
3
+ "version": "0.5.38",
4
4
  "description": "Render Manifest sites to static HTML for SEO",
5
5
  "type": "module",
6
6
  "bin": {