mnfst-render 0.5.36 → 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.
- package/manifest.render.mjs +127 -20
- package/package.json +1 -1
package/manifest.render.mjs
CHANGED
|
@@ -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);
|
|
@@ -1814,20 +1831,9 @@ function computeGlobalAssetSignature(rootDir) {
|
|
|
1814
1831
|
async function waitForVisualSettle(page, { settleMs = 300, mdWaitMs = 5000, capMs = 8000 } = {}) {
|
|
1815
1832
|
try {
|
|
1816
1833
|
await page.evaluate(async ({ settleMs, mdWaitMs, capMs }) => {
|
|
1817
|
-
//
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
while (!window.__manifestReady && Date.now() - start < capMs + 8000) {
|
|
1821
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
1822
|
-
}
|
|
1823
|
-
if (document.fonts?.ready) { try { await document.fonts.ready; } catch { /* best-effort */ } }
|
|
1824
|
-
await new Promise((r) => {
|
|
1825
|
-
const t = setTimeout(r, 250);
|
|
1826
|
-
requestAnimationFrame(() => requestAnimationFrame(() => { clearTimeout(t); r(); }));
|
|
1827
|
-
});
|
|
1828
|
-
return;
|
|
1829
|
-
}
|
|
1830
|
-
// Fallback heuristic for older framework builds:
|
|
1834
|
+
// The framework's boot signal (loader ≥0.5.190) gates the start, but it
|
|
1835
|
+
// fires once per page — per-ROUTE work is tracked by the live markdown
|
|
1836
|
+
// pending counter and the DOM heuristic below, so all three AND together.
|
|
1831
1837
|
const state = () => {
|
|
1832
1838
|
let styleLen = 0;
|
|
1833
1839
|
for (const s of document.querySelectorAll('style')) styleLen += (s.textContent || '').length;
|
|
@@ -1838,14 +1844,59 @@ async function waitForVisualSettle(page, { settleMs = 300, mdWaitMs = 5000, capM
|
|
|
1838
1844
|
const sig = `${mdEmpty}:${document.body?.innerText.length || 0}:${styleLen}:${document.fonts ? document.fonts.status : ''}`;
|
|
1839
1845
|
return { sig, mdEmpty };
|
|
1840
1846
|
};
|
|
1847
|
+
// Alpine 3.x can strand already-flushed effects when a sibling effect
|
|
1848
|
+
// creates state mid-flush — a route's x-if chain then never stamps until
|
|
1849
|
+
// something re-queues it. Bumping the data store's version re-runs every
|
|
1850
|
+
// store-reading effect: a no-op on settled pages, an unstick on stuck ones.
|
|
1851
|
+
const nudge = () => {
|
|
1852
|
+
try {
|
|
1853
|
+
if (window.Alpine?.store) {
|
|
1854
|
+
const s = window.Alpine.store('data');
|
|
1855
|
+
if (s) window.Alpine.store('data', { ...s, _dataVersion: (s._dataVersion || 0) + 1 });
|
|
1856
|
+
}
|
|
1857
|
+
} catch { /* best-effort */ }
|
|
1858
|
+
};
|
|
1859
|
+
// Responsive/variant utilities (md:row, dark:bg-*) are compiled at
|
|
1860
|
+
// runtime and the compiler throttles recompiles — a late-discovered
|
|
1861
|
+
// variant class leaves the layout in its mobile/stacked state at capture.
|
|
1862
|
+
// Hold while any variant class used in the DOM has no matching CSS rule.
|
|
1863
|
+
const unresolvedVariants = () => {
|
|
1864
|
+
try {
|
|
1865
|
+
const used = new Set();
|
|
1866
|
+
for (const el of document.querySelectorAll('[class*=":"]')) {
|
|
1867
|
+
for (const c of el.classList) if (c.includes(':') && !c.startsWith('[')) used.add(c);
|
|
1868
|
+
if (used.size > 300) break;
|
|
1869
|
+
}
|
|
1870
|
+
if (!used.size) return 0;
|
|
1871
|
+
let selectors = '';
|
|
1872
|
+
for (const sheet of document.styleSheets) {
|
|
1873
|
+
try { for (const r of sheet.cssRules) if (r.selectorText) selectors += r.selectorText + '\n'; } catch { /* cross-origin */ }
|
|
1874
|
+
}
|
|
1875
|
+
let missing = 0;
|
|
1876
|
+
for (const c of used) {
|
|
1877
|
+
if (!selectors.includes('.' + CSS.escape(c))) missing++;
|
|
1878
|
+
}
|
|
1879
|
+
return missing;
|
|
1880
|
+
} catch { return 0; }
|
|
1881
|
+
};
|
|
1841
1882
|
const start = Date.now();
|
|
1842
1883
|
let last = state();
|
|
1843
1884
|
let stableSince = Date.now();
|
|
1885
|
+
let nudged = 0;
|
|
1844
1886
|
while (Date.now() - start < capMs) {
|
|
1845
1887
|
await new Promise((r) => setTimeout(r, 100));
|
|
1888
|
+
const elapsed = Date.now() - start;
|
|
1889
|
+
if ((nudged === 0 && elapsed > 300) || (nudged === 1 && elapsed > 2000)) { nudged++; nudge(); }
|
|
1846
1890
|
const cur = state();
|
|
1891
|
+
const bootPending = !!window.__manifestReadyCoordinator && !window.__manifestReady;
|
|
1892
|
+
const inflight = (window.__manifestMarkdownPending || 0) > 0;
|
|
1847
1893
|
const mdPending = cur.mdEmpty > 0 && Date.now() - start < mdWaitMs;
|
|
1848
|
-
|
|
1894
|
+
const utilPending = (window.__manifestUtilitiesPending || 0) > 0
|
|
1895
|
+
|| (elapsed < mdWaitMs + 4000 && unresolvedVariants() > 0);
|
|
1896
|
+
// A throttled compiler can DROP the request that would resolve them —
|
|
1897
|
+
// a class mutation re-triggers its observer's schedule.
|
|
1898
|
+
if (utilPending && elapsed % 500 < 100) document.body.classList.toggle('mnfst-og-nudge');
|
|
1899
|
+
if (cur.sig !== last.sig || bootPending || inflight || mdPending || utilPending) {
|
|
1849
1900
|
last = cur;
|
|
1850
1901
|
stableSince = Date.now();
|
|
1851
1902
|
} else if (Date.now() - stableSince >= settleMs) {
|
|
@@ -1953,13 +2004,31 @@ async function takeOgSnapshot(page, outputDir, pathSeg, globalAssetSignature, ca
|
|
|
1953
2004
|
// body min-h-screen + flex grow) can otherwise screenshot as blank if
|
|
1954
2005
|
// the compositor doesn't repaint between setViewport and screenshot.
|
|
1955
2006
|
await page.evaluate(() => window.scrollTo(0, 0));
|
|
1956
|
-
|
|
2007
|
+
// Throwaway shot first: it forces the compositor to produce a fresh frame,
|
|
2008
|
+
// so the real capture below reflects the current DOM rather than a stale
|
|
2009
|
+
// surface (headless pages can sit frames behind after heavy updates).
|
|
2010
|
+
await page.screenshot({ type: 'png', clip: { x: 0, y: 0, width: 8, height: 8 }, captureBeyondViewport: false }).catch(() => { });
|
|
2011
|
+
await new Promise((r) => setTimeout(r, 120));
|
|
2012
|
+
const shoot = () => page.screenshot({
|
|
1957
2013
|
path: filePath,
|
|
1958
2014
|
type: 'png',
|
|
1959
2015
|
clip: { x: 0, y: 0, width: 1200, height: 630 },
|
|
1960
2016
|
omitBackground: false,
|
|
1961
2017
|
captureBeyondViewport: false,
|
|
1962
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
|
+
}
|
|
1963
2032
|
// Sanity check: a blank 1200×630 PNG (header only, white body) is ~8–10KB;
|
|
1964
2033
|
// a content-rich page is 50KB+. When the resulting file is suspiciously
|
|
1965
2034
|
// small the snapshot is treated as failed and the renderer falls through
|
|
@@ -3322,6 +3391,24 @@ async function runPrerender(config) {
|
|
|
3322
3391
|
const apply = () => {
|
|
3323
3392
|
try {
|
|
3324
3393
|
if (locale && typeof locale === 'string') document.documentElement.lang = locale;
|
|
3394
|
+
// View transitions freeze a snapshot layer over the updated DOM in
|
|
3395
|
+
// headless — OG screenshots then capture stale pre-route pixels.
|
|
3396
|
+
// Router opt-out for SPA swaps, plus a late <style> so the reset's
|
|
3397
|
+
// cross-document @view-transition{navigation:auto} is overridden
|
|
3398
|
+
// (appended after stylesheets; last descriptor wins).
|
|
3399
|
+
document.documentElement.setAttribute('data-no-view-transitions', '');
|
|
3400
|
+
const killCrossDocVT = () => {
|
|
3401
|
+
try {
|
|
3402
|
+
const s = document.createElement('style');
|
|
3403
|
+
s.textContent = '@view-transition { navigation: none }';
|
|
3404
|
+
(document.head || document.documentElement).appendChild(s);
|
|
3405
|
+
} catch { /* no-op */ }
|
|
3406
|
+
};
|
|
3407
|
+
if (document.readyState === 'loading') {
|
|
3408
|
+
document.addEventListener('DOMContentLoaded', killCrossDocVT, { once: true });
|
|
3409
|
+
} else {
|
|
3410
|
+
killCrossDocVT();
|
|
3411
|
+
}
|
|
3325
3412
|
} catch {
|
|
3326
3413
|
/* no-op */
|
|
3327
3414
|
}
|
|
@@ -4391,6 +4478,16 @@ async function runPrerender(config) {
|
|
|
4391
4478
|
});
|
|
4392
4479
|
});
|
|
4393
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
|
+
|
|
4394
4491
|
// Deterministic output: randomized CSS anchor-positioning names
|
|
4395
4492
|
// (--dropdown-x7f2, --popover-…, --anchor-…) are regenerated per run and
|
|
4396
4493
|
// made every render a phantom diff. Renumber them sequentially in DOM
|
|
@@ -4491,6 +4588,7 @@ async function runPrerender(config) {
|
|
|
4491
4588
|
html = stripRedundantImgSrcBindings(html);
|
|
4492
4589
|
html = stripEmptyInlineMaskStyles(html);
|
|
4493
4590
|
html = stripResolvedXIconDirectives(html);
|
|
4591
|
+
if (config.lazyIcons) html = stripLazyIconSvgs(html);
|
|
4494
4592
|
html = markPrerenderedManifestComponents(html);
|
|
4495
4593
|
|
|
4496
4594
|
// Prefix internal <a> links with the locale for non-default locales so
|
|
@@ -4734,9 +4832,18 @@ async function runPrerender(config) {
|
|
|
4734
4832
|
if (bundleUtilities) {
|
|
4735
4833
|
const utilMerged = mergeUtilityCssBlocks(utilityBlocks);
|
|
4736
4834
|
if (utilMerged.trim()) {
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
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
|
+
}
|
|
4740
4847
|
}
|
|
4741
4848
|
}
|
|
4742
4849
|
|