mnfst-render 0.5.31 → 0.5.33
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 +67 -17
- 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 } from 'node:fs';
|
|
5
|
+
import { readFileSync, readSync, mkdirSync, writeFileSync, 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';
|
|
@@ -1811,9 +1811,15 @@ function computeGlobalAssetSignature(rootDir) {
|
|
|
1811
1811
|
* - html.className (theme variant: light/dark/etc.)
|
|
1812
1812
|
*/
|
|
1813
1813
|
async function takeOgSnapshot(page, outputDir, pathSeg, globalAssetSignature, cacheDir) {
|
|
1814
|
-
|
|
1814
|
+
// The 404 page must NOT share the homepage's 'index' slug: both pages render
|
|
1815
|
+
// concurrently and would otherwise race on the same og/index.png + cache
|
|
1816
|
+
// files. The 404 (route hidden, near-empty body) screenshots blank, and that
|
|
1817
|
+
// blank would clobber — or get mis-read as — the homepage's real snapshot.
|
|
1818
|
+
const fileSeg = pathSeg === ''
|
|
1815
1819
|
? 'index'
|
|
1816
|
-
: pathSeg
|
|
1820
|
+
: pathSeg === '__404__'
|
|
1821
|
+
? '404'
|
|
1822
|
+
: pathSeg.replace(/\//g, '-').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
1817
1823
|
const ogDir = join(outputDir, 'og');
|
|
1818
1824
|
try { mkdirSync(ogDir, { recursive: true }); } catch { /* exists */ }
|
|
1819
1825
|
const filePath = join(ogDir, `${fileSeg}.png`);
|
|
@@ -1822,6 +1828,16 @@ async function takeOgSnapshot(page, outputDir, pathSeg, globalAssetSignature, ca
|
|
|
1822
1828
|
const cachePngPath = cacheDir ? join(cacheDir, `${fileSeg}.png`) : null;
|
|
1823
1829
|
const cacheHashPath = cacheDir ? join(cacheDir, `${fileSeg}.hash`) : null;
|
|
1824
1830
|
|
|
1831
|
+
// Restore the last good cached PNG into the output dir. Used whenever a fresh
|
|
1832
|
+
// snapshot can't be produced (error or blank result) so a transient failure
|
|
1833
|
+
// never leaves the page with no OG image — and never destroys the prior.
|
|
1834
|
+
const restoreFromCache = () => {
|
|
1835
|
+
if (cachePngPath && existsSync(cachePngPath)) {
|
|
1836
|
+
try { cpSync(cachePngPath, filePath); return `/og/${fileSeg}.png`; } catch { /* ignore */ }
|
|
1837
|
+
}
|
|
1838
|
+
return null;
|
|
1839
|
+
};
|
|
1840
|
+
|
|
1825
1841
|
// Cache lookup: fingerprint the rendered DOM and check against the stored
|
|
1826
1842
|
// hash. The fingerprint normalises away attribute values assigned in
|
|
1827
1843
|
// iteration order (data-hydrate-id, data-component-N) and randomly-generated
|
|
@@ -1885,11 +1901,12 @@ async function takeOgSnapshot(page, outputDir, pathSeg, globalAssetSignature, ca
|
|
|
1885
1901
|
try {
|
|
1886
1902
|
const sz = statSync(filePath).size;
|
|
1887
1903
|
if (sz < 15 * 1024) {
|
|
1888
|
-
|
|
1889
|
-
//
|
|
1890
|
-
|
|
1891
|
-
if
|
|
1892
|
-
|
|
1904
|
+
// Blank/header-only snapshot — don't trust it, but don't destroy the
|
|
1905
|
+
// good cached prior either (a transient blank must not wipe a real
|
|
1906
|
+
// image). Drop the blank output and restore the last good cached PNG;
|
|
1907
|
+
// if there's no cache, fall through to other og:image sources.
|
|
1908
|
+
try { unlinkSync(filePath); } catch { /* missing is fine */ }
|
|
1909
|
+
return restoreFromCache();
|
|
1893
1910
|
}
|
|
1894
1911
|
} catch { /* stat failure is non-fatal */ }
|
|
1895
1912
|
// Populate the cache: copy the fresh PNG into the cache dir and write the
|
|
@@ -1903,10 +1920,12 @@ async function takeOgSnapshot(page, outputDir, pathSeg, globalAssetSignature, ca
|
|
|
1903
1920
|
}
|
|
1904
1921
|
return `/og/${fileSeg}.png`;
|
|
1905
1922
|
} catch (e) {
|
|
1906
|
-
// Failures here are non-fatal
|
|
1907
|
-
//
|
|
1923
|
+
// Failures here are non-fatal and must be non-destructive: prefer the last
|
|
1924
|
+
// good cached PNG (restored into the output) over leaving the page with no
|
|
1925
|
+
// OG image. Only if there's no cache do we fall through to other og:image
|
|
1926
|
+
// sources (manifest icon, first content <img>, etc.).
|
|
1908
1927
|
console.error(`prerender: og snapshot failed for /${pathSeg || ''}: ${e?.message || e}`);
|
|
1909
|
-
return
|
|
1928
|
+
return restoreFromCache();
|
|
1910
1929
|
}
|
|
1911
1930
|
}
|
|
1912
1931
|
|
|
@@ -2101,6 +2120,10 @@ async function injectMetaInDom(page, ctx) {
|
|
|
2101
2120
|
} else if (typeof expr === 'boolean' || typeof expr === 'number') {
|
|
2102
2121
|
return String(expr);
|
|
2103
2122
|
}
|
|
2123
|
+
// Image only: the per-page auto-snapshot outranks the generic static
|
|
2124
|
+
// fallback.image — it depicts this exact page and is sized for OG cards.
|
|
2125
|
+
// The fallback then only applies to pages with no snapshot.
|
|
2126
|
+
if (key === 'image' && ctx.snapshotUrl) return ctx.snapshotUrl;
|
|
2104
2127
|
// Layer 4: explicit fallback
|
|
2105
2128
|
const fallback = exprMap.fallback?.[key];
|
|
2106
2129
|
if (fallback) return String(fallback);
|
|
@@ -2989,7 +3012,7 @@ async function runPrerender(config) {
|
|
|
2989
3012
|
return;
|
|
2990
3013
|
}
|
|
2991
3014
|
|
|
2992
|
-
const
|
|
3015
|
+
const finalOutput = resolve(config.output);
|
|
2993
3016
|
const rootResolved = resolve(config.root);
|
|
2994
3017
|
// Router base = URL pathname to the app root. When dist is deployed as site root (e.g. Appwrite), use "".
|
|
2995
3018
|
// Set manifest.prerender.routerBase only when the app is served from a subpath (e.g. /app).
|
|
@@ -3001,11 +3024,27 @@ async function runPrerender(config) {
|
|
|
3001
3024
|
routerBasePath = '';
|
|
3002
3025
|
}
|
|
3003
3026
|
|
|
3004
|
-
|
|
3005
|
-
|
|
3027
|
+
// Atomic output: build into a sibling staging dir, then swap it into place
|
|
3028
|
+
// only after every step below succeeds (see the end of this function). A
|
|
3029
|
+
// crashed or interrupted render therefore never leaves a half-written or empty
|
|
3030
|
+
// output dir for mnfst-publish to ship — the previous good build stays put
|
|
3031
|
+
// until the new one is complete.
|
|
3032
|
+
const stagingOutput = finalOutput + '.mnfst-staging';
|
|
3033
|
+
if (existsSync(stagingOutput)) rmSync(stagingOutput, { recursive: true });
|
|
3034
|
+
mkdirSync(stagingOutput, { recursive: true });
|
|
3035
|
+
// Redirect ALL downstream writes (both config.output and outputResolved) to
|
|
3036
|
+
// the staging dir for the remainder of the build.
|
|
3037
|
+
config.output = stagingOutput;
|
|
3038
|
+
const outputResolved = stagingOutput;
|
|
3039
|
+
// Don't copy the previous build (the final output dir) into staging — only the
|
|
3040
|
+
// staging dir's own basename is excluded by copyProjectIntoDist.
|
|
3041
|
+
const finalBasename = basename(finalOutput);
|
|
3042
|
+
COPY_EXCLUDE.add(finalBasename);
|
|
3043
|
+
try {
|
|
3044
|
+
copyProjectIntoDist(rootResolved, outputResolved);
|
|
3045
|
+
} finally {
|
|
3046
|
+
COPY_EXCLUDE.delete(finalBasename);
|
|
3006
3047
|
}
|
|
3007
|
-
mkdirSync(outputResolved, { recursive: true });
|
|
3008
|
-
copyProjectIntoDist(rootResolved, outputResolved);
|
|
3009
3048
|
// Env placeholders (${VAR}) only resolve at runtime via window.env (populated
|
|
3010
3049
|
// by the mnfst-run dev server from .env). A static prod build has no
|
|
3011
3050
|
// window.env, so any ${VAR} left in the shipped manifest.json stays literal
|
|
@@ -3418,8 +3457,13 @@ async function runPrerender(config) {
|
|
|
3418
3457
|
// by data-head, prerender.meta config, or an explicit fallback.
|
|
3419
3458
|
let earlySnapshotUrl = null;
|
|
3420
3459
|
if (config.seo.imageSnapshots) {
|
|
3460
|
+
// Only an EXPLICIT per-page image suppresses the auto-snapshot:
|
|
3461
|
+
// prerender.meta.image (a config expression) or an og:image already in
|
|
3462
|
+
// the head (data-head / index.html). A generic prerender.meta.fallback
|
|
3463
|
+
// .image must NOT skip it — the per-page snapshot is the primary image
|
|
3464
|
+
// and the static fallback is only used when a snapshot isn't available
|
|
3465
|
+
// (see resolve('image') in injectMetaInDom).
|
|
3421
3466
|
const ogImageHandled = !!config.seo.meta?.image
|
|
3422
|
-
|| !!config.seo.meta?.fallback?.image
|
|
3423
3467
|
|| await page.evaluate(() => !!document.head.querySelector('meta[property="og:image"]'));
|
|
3424
3468
|
if (!ogImageHandled) {
|
|
3425
3469
|
earlySnapshotUrl = await takeOgSnapshot(page, config.output, is404 ? '__404__' : pathSeg, globalAssetSig, ogCacheDir);
|
|
@@ -4593,6 +4637,12 @@ async function runPrerender(config) {
|
|
|
4593
4637
|
writeFileSync(join(config.output, '_redirects'), lines.join('\n'), 'utf8');
|
|
4594
4638
|
}
|
|
4595
4639
|
|
|
4640
|
+
// Atomic swap: replace the previous output with the freshly built staging dir.
|
|
4641
|
+
// rename() is atomic on the same filesystem (staging is a sibling of output),
|
|
4642
|
+
// so a consumer never sees a partially written output directory.
|
|
4643
|
+
if (existsSync(finalOutput)) rmSync(finalOutput, { recursive: true });
|
|
4644
|
+
renameSync(stagingOutput, finalOutput);
|
|
4645
|
+
config.output = finalOutput;
|
|
4596
4646
|
}
|
|
4597
4647
|
|
|
4598
4648
|
// Auto-run main() when this file is the direct entry point (node manifest.render.mjs)
|