mnfst-render 0.5.35 → 0.5.37

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.
Files changed (2) hide show
  1. package/manifest.render.mjs +127 -1
  2. package/package.json +1 -1
@@ -1814,6 +1814,9 @@ function computeGlobalAssetSignature(rootDir) {
1814
1814
  async function waitForVisualSettle(page, { settleMs = 300, mdWaitMs = 5000, capMs = 8000 } = {}) {
1815
1815
  try {
1816
1816
  await page.evaluate(async ({ settleMs, mdWaitMs, capMs }) => {
1817
+ // The framework's boot signal (loader ≥0.5.190) gates the start, but it
1818
+ // fires once per page — per-ROUTE work is tracked by the live markdown
1819
+ // pending counter and the DOM heuristic below, so all three AND together.
1817
1820
  const state = () => {
1818
1821
  let styleLen = 0;
1819
1822
  for (const s of document.querySelectorAll('style')) styleLen += (s.textContent || '').length;
@@ -1824,14 +1827,58 @@ async function waitForVisualSettle(page, { settleMs = 300, mdWaitMs = 5000, capM
1824
1827
  const sig = `${mdEmpty}:${document.body?.innerText.length || 0}:${styleLen}:${document.fonts ? document.fonts.status : ''}`;
1825
1828
  return { sig, mdEmpty };
1826
1829
  };
1830
+ // Alpine 3.x can strand already-flushed effects when a sibling effect
1831
+ // creates state mid-flush — a route's x-if chain then never stamps until
1832
+ // something re-queues it. Bumping the data store's version re-runs every
1833
+ // store-reading effect: a no-op on settled pages, an unstick on stuck ones.
1834
+ const nudge = () => {
1835
+ try {
1836
+ if (window.Alpine?.store) {
1837
+ const s = window.Alpine.store('data');
1838
+ if (s) window.Alpine.store('data', { ...s, _dataVersion: (s._dataVersion || 0) + 1 });
1839
+ }
1840
+ } catch { /* best-effort */ }
1841
+ };
1842
+ // Responsive/variant utilities (md:row, dark:bg-*) are compiled at
1843
+ // runtime and the compiler throttles recompiles — a late-discovered
1844
+ // variant class leaves the layout in its mobile/stacked state at capture.
1845
+ // Hold while any variant class used in the DOM has no matching CSS rule.
1846
+ const unresolvedVariants = () => {
1847
+ try {
1848
+ const used = new Set();
1849
+ for (const el of document.querySelectorAll('[class*=":"]')) {
1850
+ for (const c of el.classList) if (c.includes(':') && !c.startsWith('[')) used.add(c);
1851
+ if (used.size > 300) break;
1852
+ }
1853
+ if (!used.size) return 0;
1854
+ let selectors = '';
1855
+ for (const sheet of document.styleSheets) {
1856
+ try { for (const r of sheet.cssRules) if (r.selectorText) selectors += r.selectorText + '\n'; } catch { /* cross-origin */ }
1857
+ }
1858
+ let missing = 0;
1859
+ for (const c of used) {
1860
+ if (!selectors.includes('.' + CSS.escape(c))) missing++;
1861
+ }
1862
+ return missing;
1863
+ } catch { return 0; }
1864
+ };
1827
1865
  const start = Date.now();
1828
1866
  let last = state();
1829
1867
  let stableSince = Date.now();
1868
+ let nudged = 0;
1830
1869
  while (Date.now() - start < capMs) {
1831
1870
  await new Promise((r) => setTimeout(r, 100));
1871
+ const elapsed = Date.now() - start;
1872
+ if ((nudged === 0 && elapsed > 300) || (nudged === 1 && elapsed > 2000)) { nudged++; nudge(); }
1832
1873
  const cur = state();
1874
+ const bootPending = !!window.__manifestReadyCoordinator && !window.__manifestReady;
1875
+ const inflight = (window.__manifestMarkdownPending || 0) > 0;
1833
1876
  const mdPending = cur.mdEmpty > 0 && Date.now() - start < mdWaitMs;
1834
- if (cur.sig !== last.sig || mdPending) {
1877
+ const utilPending = elapsed < mdWaitMs + 4000 && unresolvedVariants() > 0;
1878
+ // A throttled compiler can DROP the request that would resolve them —
1879
+ // a class mutation re-triggers its observer's schedule.
1880
+ if (utilPending && elapsed % 500 < 100) document.body.classList.toggle('mnfst-og-nudge');
1881
+ if (cur.sig !== last.sig || bootPending || inflight || mdPending || utilPending) {
1835
1882
  last = cur;
1836
1883
  stableSince = Date.now();
1837
1884
  } else if (Date.now() - stableSince >= settleMs) {
@@ -1939,6 +1986,11 @@ async function takeOgSnapshot(page, outputDir, pathSeg, globalAssetSignature, ca
1939
1986
  // body min-h-screen + flex grow) can otherwise screenshot as blank if
1940
1987
  // the compositor doesn't repaint between setViewport and screenshot.
1941
1988
  await page.evaluate(() => window.scrollTo(0, 0));
1989
+ // Throwaway shot first: it forces the compositor to produce a fresh frame,
1990
+ // so the real capture below reflects the current DOM rather than a stale
1991
+ // surface (headless pages can sit frames behind after heavy updates).
1992
+ await page.screenshot({ type: 'png', clip: { x: 0, y: 0, width: 8, height: 8 }, captureBeyondViewport: false }).catch(() => { });
1993
+ await new Promise((r) => setTimeout(r, 120));
1942
1994
  await page.screenshot({
1943
1995
  path: filePath,
1944
1996
  type: 'png',
@@ -3308,6 +3360,24 @@ async function runPrerender(config) {
3308
3360
  const apply = () => {
3309
3361
  try {
3310
3362
  if (locale && typeof locale === 'string') document.documentElement.lang = locale;
3363
+ // View transitions freeze a snapshot layer over the updated DOM in
3364
+ // headless — OG screenshots then capture stale pre-route pixels.
3365
+ // Router opt-out for SPA swaps, plus a late <style> so the reset's
3366
+ // cross-document @view-transition{navigation:auto} is overridden
3367
+ // (appended after stylesheets; last descriptor wins).
3368
+ document.documentElement.setAttribute('data-no-view-transitions', '');
3369
+ const killCrossDocVT = () => {
3370
+ try {
3371
+ const s = document.createElement('style');
3372
+ s.textContent = '@view-transition { navigation: none }';
3373
+ (document.head || document.documentElement).appendChild(s);
3374
+ } catch { /* no-op */ }
3375
+ };
3376
+ if (document.readyState === 'loading') {
3377
+ document.addEventListener('DOMContentLoaded', killCrossDocVT, { once: true });
3378
+ } else {
3379
+ killCrossDocVT();
3380
+ }
3311
3381
  } catch {
3312
3382
  /* no-op */
3313
3383
  }
@@ -4352,6 +4422,62 @@ async function runPrerender(config) {
4352
4422
  if (tpl.hasAttribute('data-hydrate') || tpl.closest('[data-hydrate]')) return;
4353
4423
  tag(tpl._x_currentIfEl);
4354
4424
  });
4425
+
4426
+ // Clones inside CLOSED popovers carry no crawler value and are wiped
4427
+ // at boot anyway (Alpine re-renders from the template) — drop them from
4428
+ // the output entirely.
4429
+ document.querySelectorAll('[data-mnfst-prerender-clone]').forEach((el) => {
4430
+ if (!document.contains(el)) return;
4431
+ const pop = el.closest('[popover]');
4432
+ if (pop && !pop.matches(':popover-open')) el.remove();
4433
+ });
4434
+
4435
+ // Containers stamped data-mnfst-generated are fully rebuilt by their
4436
+ // plugin at boot (e.g. a colorpicker's palette library — thousands of
4437
+ // swatch nodes with full palette JSON in x-data). Serialize them empty.
4438
+ document.querySelectorAll('[data-mnfst-generated]').forEach((el) => {
4439
+ if (document.contains(el)) el.innerHTML = '';
4440
+ });
4441
+
4442
+ // Legacy bridge for colorpickers older than the stamp (<0.5.190): the
4443
+ // palette library wrapper is rebuilt from scratch at boot regardless,
4444
+ // so drop it from the output. Remove once sites pin a stamped build.
4445
+ document.querySelectorAll('.library-wrapper').forEach((el) => {
4446
+ if (el.querySelector('.library-palette, [data-cp-library-group]')) el.remove();
4447
+ });
4448
+ });
4449
+
4450
+ // Deterministic output: randomized CSS anchor-positioning names
4451
+ // (--dropdown-x7f2, --popover-…, --anchor-…) are regenerated per run and
4452
+ // made every render a phantom diff. Renumber them sequentially in DOM
4453
+ // order so identical content serializes identically.
4454
+ await page.evaluate(() => {
4455
+ const RE = /--(dropdown|popover|anchor)-[a-z0-9]+/g;
4456
+ const map = new Map();
4457
+ const counters = { dropdown: 0, popover: 0, anchor: 0 };
4458
+ const rename = (m, kind) => {
4459
+ if (!map.has(m)) map.set(m, `--${kind}-p${++counters[kind]}`);
4460
+ return map.get(m);
4461
+ };
4462
+ for (const el of document.querySelectorAll('*')) {
4463
+ for (const attr of el.attributes) {
4464
+ if (!attr.value || attr.value.indexOf('--') === -1) continue;
4465
+ const next = attr.value.replace(RE, (m, kind) => rename(m, kind));
4466
+ if (next !== attr.value) el.setAttribute(attr.name, next);
4467
+ }
4468
+ }
4469
+
4470
+ // Component bookkeeping attrs land in whichever order the swap raced —
4471
+ // re-set the pair canonically so it can't flip between runs. (They are
4472
+ // not Alpine directives; position carries no behavior.)
4473
+ for (const el of document.querySelectorAll('[data-component][data-order]')) {
4474
+ const c = el.getAttribute('data-component');
4475
+ const o = el.getAttribute('data-order');
4476
+ el.removeAttribute('data-component');
4477
+ el.removeAttribute('data-order');
4478
+ el.setAttribute('data-component', c);
4479
+ el.setAttribute('data-order', o);
4480
+ }
4355
4481
  });
4356
4482
 
4357
4483
  // SEO / AEO meta injection — see resolveConfig().seo for precedence layers.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-render",
3
- "version": "0.5.35",
3
+ "version": "0.5.37",
4
4
  "description": "Render Manifest sites to static HTML for SEO",
5
5
  "type": "module",
6
6
  "bin": {