mnfst-render 0.5.34 → 0.5.36
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 +131 -1
- package/package.json +1 -1
package/manifest.render.mjs
CHANGED
|
@@ -1800,6 +1800,70 @@ function computeGlobalAssetSignature(rootDir) {
|
|
|
1800
1800
|
return sha(parts.join('|'));
|
|
1801
1801
|
}
|
|
1802
1802
|
|
|
1803
|
+
/**
|
|
1804
|
+
* Wait for the page to visually settle before OG fingerprint/screenshot.
|
|
1805
|
+
* manifest:render-ready covers tracked data sources, but async work can land
|
|
1806
|
+
* after it — x-markdown article fetches, runtime-compiled utility CSS, and
|
|
1807
|
+
* webfonts — and screenshotting early captures a blank article region. Two
|
|
1808
|
+
* conditions gate the snapshot: every [x-markdown] region has rendered
|
|
1809
|
+
* content (or the cap expires — some regions are legitimately empty in
|
|
1810
|
+
* prerender, e.g. demos), and a cheap DOM signature (text length + injected
|
|
1811
|
+
* style bytes + font status + empty-markdown count) holds steady. Then let
|
|
1812
|
+
* fonts finish and the compositor paint.
|
|
1813
|
+
*/
|
|
1814
|
+
async function waitForVisualSettle(page, { settleMs = 300, mdWaitMs = 5000, capMs = 8000 } = {}) {
|
|
1815
|
+
try {
|
|
1816
|
+
await page.evaluate(async ({ settleMs, mdWaitMs, capMs }) => {
|
|
1817
|
+
// Preferred: the framework's own settled signal (loader ≥0.5.190).
|
|
1818
|
+
if (window.__manifestReadyCoordinator) {
|
|
1819
|
+
const start = Date.now();
|
|
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:
|
|
1831
|
+
const state = () => {
|
|
1832
|
+
let styleLen = 0;
|
|
1833
|
+
for (const s of document.querySelectorAll('style')) styleLen += (s.textContent || '').length;
|
|
1834
|
+
let mdEmpty = 0;
|
|
1835
|
+
for (const e of document.querySelectorAll('[x-markdown]')) {
|
|
1836
|
+
if (!e.innerHTML.trim()) mdEmpty++;
|
|
1837
|
+
}
|
|
1838
|
+
const sig = `${mdEmpty}:${document.body?.innerText.length || 0}:${styleLen}:${document.fonts ? document.fonts.status : ''}`;
|
|
1839
|
+
return { sig, mdEmpty };
|
|
1840
|
+
};
|
|
1841
|
+
const start = Date.now();
|
|
1842
|
+
let last = state();
|
|
1843
|
+
let stableSince = Date.now();
|
|
1844
|
+
while (Date.now() - start < capMs) {
|
|
1845
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1846
|
+
const cur = state();
|
|
1847
|
+
const mdPending = cur.mdEmpty > 0 && Date.now() - start < mdWaitMs;
|
|
1848
|
+
if (cur.sig !== last.sig || mdPending) {
|
|
1849
|
+
last = cur;
|
|
1850
|
+
stableSince = Date.now();
|
|
1851
|
+
} else if (Date.now() - stableSince >= settleMs) {
|
|
1852
|
+
break;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
if (document.fonts?.ready) { try { await document.fonts.ready; } catch { /* best-effort */ } }
|
|
1856
|
+
// Let the compositor paint the settled state — but raced against a
|
|
1857
|
+
// timeout: a frame-idle headless page may never fire rAF, and an
|
|
1858
|
+
// unresolved evaluate() wedges the whole render worker.
|
|
1859
|
+
await new Promise((r) => {
|
|
1860
|
+
const t = setTimeout(r, 250);
|
|
1861
|
+
requestAnimationFrame(() => requestAnimationFrame(() => { clearTimeout(t); r(); }));
|
|
1862
|
+
});
|
|
1863
|
+
}, { settleMs, mdWaitMs, capMs });
|
|
1864
|
+
} catch { /* settle is best-effort — never blocks the snapshot */ }
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1803
1867
|
/**
|
|
1804
1868
|
* Snapshot the page at 1200×630 and write to <output>/og/<slug>.png. Cache
|
|
1805
1869
|
* sidecar lives in <root>/.mnfst-cache/og/ — outside the output dir, which is
|
|
@@ -1811,6 +1875,10 @@ function computeGlobalAssetSignature(rootDir) {
|
|
|
1811
1875
|
* - html.className (theme variant: light/dark/etc.)
|
|
1812
1876
|
*/
|
|
1813
1877
|
async function takeOgSnapshot(page, outputDir, pathSeg, globalAssetSignature, cacheDir) {
|
|
1878
|
+
// Settle BEFORE the cache fingerprint: the hash must describe the same
|
|
1879
|
+
// steady-state DOM the screenshot captures, or late-arriving content makes
|
|
1880
|
+
// the cache key unstable across runs.
|
|
1881
|
+
await waitForVisualSettle(page);
|
|
1814
1882
|
// The 404 page must NOT share the homepage's 'index' slug: both pages render
|
|
1815
1883
|
// concurrently and would otherwise race on the same og/index.png + cache
|
|
1816
1884
|
// files. The 404 (route hidden, near-empty body) screenshots blank, and that
|
|
@@ -2836,9 +2904,12 @@ function startStaticServer(rootDir) {
|
|
|
2836
2904
|
}
|
|
2837
2905
|
|
|
2838
2906
|
// --- Copy project into output so website is self-contained (e.g. for Appwrite). ---
|
|
2907
|
+
// Internal/tooling files stay out of the published output: agent instructions,
|
|
2908
|
+
// the schema copy, and OS metadata anywhere in the tree.
|
|
2839
2909
|
const COPY_EXCLUDE = new Set([
|
|
2840
2910
|
'node_modules', '.git', 'package.json', 'package-lock.json',
|
|
2841
2911
|
'index.html', 'prerender.mjs', 'prerender.js', '_redirects',
|
|
2912
|
+
'CLAUDE.md', 'AGENTS.md', 'manifest.schema.json',
|
|
2842
2913
|
]);
|
|
2843
2914
|
|
|
2844
2915
|
function copyProjectIntoDist(rootResolved, outputResolved) {
|
|
@@ -2850,7 +2921,10 @@ function copyProjectIntoDist(rootResolved, outputResolved) {
|
|
|
2850
2921
|
if (COPY_EXCLUDE.has(name) || name.startsWith('.')) continue;
|
|
2851
2922
|
const src = join(rootResolved, name);
|
|
2852
2923
|
const dest = join(outputResolved, name);
|
|
2853
|
-
cpSync(src, dest, {
|
|
2924
|
+
cpSync(src, dest, {
|
|
2925
|
+
recursive: true,
|
|
2926
|
+
filter: (p) => { const b = basename(p); return b !== '.DS_Store' && b !== 'Thumbs.db'; },
|
|
2927
|
+
});
|
|
2854
2928
|
}
|
|
2855
2929
|
COPY_EXCLUDE.delete(outputDirName);
|
|
2856
2930
|
}
|
|
@@ -4292,6 +4366,62 @@ async function runPrerender(config) {
|
|
|
4292
4366
|
if (tpl.hasAttribute('data-hydrate') || tpl.closest('[data-hydrate]')) return;
|
|
4293
4367
|
tag(tpl._x_currentIfEl);
|
|
4294
4368
|
});
|
|
4369
|
+
|
|
4370
|
+
// Clones inside CLOSED popovers carry no crawler value and are wiped
|
|
4371
|
+
// at boot anyway (Alpine re-renders from the template) — drop them from
|
|
4372
|
+
// the output entirely.
|
|
4373
|
+
document.querySelectorAll('[data-mnfst-prerender-clone]').forEach((el) => {
|
|
4374
|
+
if (!document.contains(el)) return;
|
|
4375
|
+
const pop = el.closest('[popover]');
|
|
4376
|
+
if (pop && !pop.matches(':popover-open')) el.remove();
|
|
4377
|
+
});
|
|
4378
|
+
|
|
4379
|
+
// Containers stamped data-mnfst-generated are fully rebuilt by their
|
|
4380
|
+
// plugin at boot (e.g. a colorpicker's palette library — thousands of
|
|
4381
|
+
// swatch nodes with full palette JSON in x-data). Serialize them empty.
|
|
4382
|
+
document.querySelectorAll('[data-mnfst-generated]').forEach((el) => {
|
|
4383
|
+
if (document.contains(el)) el.innerHTML = '';
|
|
4384
|
+
});
|
|
4385
|
+
|
|
4386
|
+
// Legacy bridge for colorpickers older than the stamp (<0.5.190): the
|
|
4387
|
+
// palette library wrapper is rebuilt from scratch at boot regardless,
|
|
4388
|
+
// so drop it from the output. Remove once sites pin a stamped build.
|
|
4389
|
+
document.querySelectorAll('.library-wrapper').forEach((el) => {
|
|
4390
|
+
if (el.querySelector('.library-palette, [data-cp-library-group]')) el.remove();
|
|
4391
|
+
});
|
|
4392
|
+
});
|
|
4393
|
+
|
|
4394
|
+
// Deterministic output: randomized CSS anchor-positioning names
|
|
4395
|
+
// (--dropdown-x7f2, --popover-…, --anchor-…) are regenerated per run and
|
|
4396
|
+
// made every render a phantom diff. Renumber them sequentially in DOM
|
|
4397
|
+
// order so identical content serializes identically.
|
|
4398
|
+
await page.evaluate(() => {
|
|
4399
|
+
const RE = /--(dropdown|popover|anchor)-[a-z0-9]+/g;
|
|
4400
|
+
const map = new Map();
|
|
4401
|
+
const counters = { dropdown: 0, popover: 0, anchor: 0 };
|
|
4402
|
+
const rename = (m, kind) => {
|
|
4403
|
+
if (!map.has(m)) map.set(m, `--${kind}-p${++counters[kind]}`);
|
|
4404
|
+
return map.get(m);
|
|
4405
|
+
};
|
|
4406
|
+
for (const el of document.querySelectorAll('*')) {
|
|
4407
|
+
for (const attr of el.attributes) {
|
|
4408
|
+
if (!attr.value || attr.value.indexOf('--') === -1) continue;
|
|
4409
|
+
const next = attr.value.replace(RE, (m, kind) => rename(m, kind));
|
|
4410
|
+
if (next !== attr.value) el.setAttribute(attr.name, next);
|
|
4411
|
+
}
|
|
4412
|
+
}
|
|
4413
|
+
|
|
4414
|
+
// Component bookkeeping attrs land in whichever order the swap raced —
|
|
4415
|
+
// re-set the pair canonically so it can't flip between runs. (They are
|
|
4416
|
+
// not Alpine directives; position carries no behavior.)
|
|
4417
|
+
for (const el of document.querySelectorAll('[data-component][data-order]')) {
|
|
4418
|
+
const c = el.getAttribute('data-component');
|
|
4419
|
+
const o = el.getAttribute('data-order');
|
|
4420
|
+
el.removeAttribute('data-component');
|
|
4421
|
+
el.removeAttribute('data-order');
|
|
4422
|
+
el.setAttribute('data-component', c);
|
|
4423
|
+
el.setAttribute('data-order', o);
|
|
4424
|
+
}
|
|
4295
4425
|
});
|
|
4296
4426
|
|
|
4297
4427
|
// SEO / AEO meta injection — see resolveConfig().seo for precedence layers.
|