mnfst-render 0.5.1 → 0.5.3
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 +155 -35
- package/package.json +1 -1
package/manifest.render.mjs
CHANGED
|
@@ -172,6 +172,7 @@ function parseArgs() {
|
|
|
172
172
|
if (args[i] === '--wait' && args[i + 1]) { out.wait = parseInt(args[++i], 10); continue; }
|
|
173
173
|
if (args[i] === '--wait-after-idle' && args[i + 1]) { out.waitAfterIdle = parseInt(args[++i], 10); continue; }
|
|
174
174
|
if (args[i] === '--concurrency' && args[i + 1]) { out.concurrency = parseInt(args[++i], 10); continue; }
|
|
175
|
+
if (args[i] === '--retries' && args[i + 1]) { out.retries = parseInt(args[++i], 10); continue; }
|
|
175
176
|
if (args[i] === '--dry-run') { out.dryRun = true; continue; }
|
|
176
177
|
if (args[i] === '--debug-prerender') { out.debugPrerender = true; continue; }
|
|
177
178
|
}
|
|
@@ -231,6 +232,7 @@ function resolveConfig() {
|
|
|
231
232
|
wait: cli.wait ?? pre.wait ?? null,
|
|
232
233
|
waitAfterIdle: 0,
|
|
233
234
|
concurrency: Math.max(1, cli.concurrency ?? pre.concurrency ?? Math.max(4, cpus().length - 1)),
|
|
235
|
+
retries: Math.max(0, cli.retries ?? pre.retries ?? 2),
|
|
234
236
|
localeSubstitution: true,
|
|
235
237
|
localeSubstitutionExclude: [],
|
|
236
238
|
/** Explicit locale-neutral paths to render in addition to those discovered automatically.
|
|
@@ -1751,8 +1753,9 @@ async function runPrerender(config) {
|
|
|
1751
1753
|
browser = await puppeteer.default.launch({ headless: true });
|
|
1752
1754
|
}
|
|
1753
1755
|
|
|
1754
|
-
const timeout = config.wait ??
|
|
1756
|
+
const timeout = config.wait ?? 30000;
|
|
1755
1757
|
const concurrency = config.concurrency;
|
|
1758
|
+
const maxRetries = config.retries ?? 2;
|
|
1756
1759
|
const pathTotal = pathList.length;
|
|
1757
1760
|
const failedPaths = [];
|
|
1758
1761
|
const debugRows = [];
|
|
@@ -1883,6 +1886,36 @@ async function runPrerender(config) {
|
|
|
1883
1886
|
let nextId = 0;
|
|
1884
1887
|
const skipTags = new Set(['MAIN', 'BODY', 'HTML']);
|
|
1885
1888
|
|
|
1889
|
+
// Own MutationObserver registered before any other script on the page.
|
|
1890
|
+
// This guarantees we process DOM additions before Alpine's observer does —
|
|
1891
|
+
// critically, before Alpine's observer calls initTree on newly expanded
|
|
1892
|
+
// Manifest components (preloaded or lazy) and bakes their `:class` state.
|
|
1893
|
+
const installHydrateObserver = () => {
|
|
1894
|
+
if (window.__manifestHydrateObserver || !document.body) return;
|
|
1895
|
+
const obs = new MutationObserver((mutations) => {
|
|
1896
|
+
for (const m of mutations) {
|
|
1897
|
+
if (m.type !== 'childList') continue;
|
|
1898
|
+
for (const node of m.addedNodes) {
|
|
1899
|
+
if (node.nodeType !== 1) continue;
|
|
1900
|
+
try { snapshotSubtree(node); } catch (_) {}
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
});
|
|
1904
|
+
obs.observe(document.body, { childList: true, subtree: true });
|
|
1905
|
+
window.__manifestHydrateObserver = obs;
|
|
1906
|
+
};
|
|
1907
|
+
if (typeof document !== 'undefined') {
|
|
1908
|
+
if (document.body) {
|
|
1909
|
+
installHydrateObserver();
|
|
1910
|
+
} else {
|
|
1911
|
+
document.addEventListener('DOMContentLoaded', installHydrateObserver, { once: true });
|
|
1912
|
+
// Also try once readyState flips to interactive
|
|
1913
|
+
document.addEventListener('readystatechange', () => {
|
|
1914
|
+
if (document.readyState !== 'loading') installHydrateObserver();
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1886
1919
|
const snapshotElement = (el) => {
|
|
1887
1920
|
if (!el || el.nodeType !== 1) return;
|
|
1888
1921
|
if (el.hasAttribute('data-manifest-hyd-id')) return; // already snapshotted
|
|
@@ -1894,7 +1927,7 @@ async function runPrerender(config) {
|
|
|
1894
1927
|
if (a.name === 'data-manifest-hyd-id') continue;
|
|
1895
1928
|
attrs[a.name] = a.value;
|
|
1896
1929
|
}
|
|
1897
|
-
allSnapshots.push({ id, attrs });
|
|
1930
|
+
allSnapshots.push({ id, tag: el.tagName, attrs });
|
|
1898
1931
|
};
|
|
1899
1932
|
|
|
1900
1933
|
const snapshotElementAndDescendants = (el) => {
|
|
@@ -1943,24 +1976,38 @@ async function runPrerender(config) {
|
|
|
1943
1976
|
window.__manifestHydrateSnapshots = allSnapshots;
|
|
1944
1977
|
};
|
|
1945
1978
|
|
|
1946
|
-
// Wrap Alpine.
|
|
1947
|
-
//
|
|
1948
|
-
//
|
|
1949
|
-
//
|
|
1950
|
-
//
|
|
1951
|
-
//
|
|
1979
|
+
// Wrap Alpine.start so the snapshot runs INSIDE the start call, before
|
|
1980
|
+
// Alpine has a chance to walk and mutate the tree. alpine:init as an
|
|
1981
|
+
// external hook proved unreliable — in some configurations it fires after
|
|
1982
|
+
// Alpine has already processed some elements. Alpine.start is the single
|
|
1983
|
+
// synchronous entry point for the initial walk, so wrapping it guarantees
|
|
1984
|
+
// we capture source state before any directive has been applied.
|
|
1985
|
+
//
|
|
1986
|
+
// We also wrap Alpine.initTree for lazy-loaded components that appear in
|
|
1987
|
+
// the DOM after Alpine.start() has completed (fetched by the components
|
|
1988
|
+
// plugin in response to new <x-*> placeholders).
|
|
1989
|
+
//
|
|
1990
|
+
// Both wraps are installed via a defineProperty setter on window.Alpine
|
|
1991
|
+
// so they land the instant Alpine's CDN script does `window.Alpine = ...`.
|
|
1952
1992
|
const wrap = (alpine) => {
|
|
1953
1993
|
if (!alpine || alpine.__manifestRenderWrapped) return;
|
|
1954
|
-
if (typeof alpine.initTree !== 'function') return;
|
|
1955
1994
|
alpine.__manifestRenderWrapped = true;
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1995
|
+
if (typeof alpine.start === 'function') {
|
|
1996
|
+
const originalStart = alpine.start.bind(alpine);
|
|
1997
|
+
alpine.start = function () {
|
|
1998
|
+
try { snapshotSubtree(document.body); } catch (_) { /* graceful */ }
|
|
1999
|
+
return originalStart.apply(this, arguments);
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
if (typeof alpine.initTree === 'function') {
|
|
2003
|
+
const originalInit = alpine.initTree.bind(alpine);
|
|
2004
|
+
alpine.initTree = function (root) {
|
|
2005
|
+
try { snapshotSubtree(root || document.body); } catch (_) { /* graceful */ }
|
|
2006
|
+
return originalInit.apply(this, arguments);
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
1961
2009
|
};
|
|
1962
2010
|
|
|
1963
|
-
// Setter trap: as soon as Alpine is assigned to window, wrap it.
|
|
1964
2011
|
let _Alpine;
|
|
1965
2012
|
try {
|
|
1966
2013
|
Object.defineProperty(window, 'Alpine', {
|
|
@@ -1971,9 +2018,8 @@ async function runPrerender(config) {
|
|
|
1971
2018
|
});
|
|
1972
2019
|
} catch (_) { /* defineProperty failed, fall back to event listeners */ }
|
|
1973
2020
|
|
|
1974
|
-
// Belt-and-braces: also try to wrap on alpine:init events in case some
|
|
1975
|
-
// environment beats the setter. wrap() is idempotent.
|
|
1976
2021
|
if (typeof document !== 'undefined') {
|
|
2022
|
+
// Event-based fallback in case the setter trap missed Alpine assignment.
|
|
1977
2023
|
document.addEventListener('alpine:init', () => wrap(window.Alpine));
|
|
1978
2024
|
document.addEventListener('alpine:initialized', () => wrap(window.Alpine));
|
|
1979
2025
|
}
|
|
@@ -2218,22 +2264,81 @@ async function runPrerender(config) {
|
|
|
2218
2264
|
// it would in the live SPA. After restoring source attributes we re-add the
|
|
2219
2265
|
// `data-prerender-hydrate` marker so downstream Node.js stripping passes
|
|
2220
2266
|
// continue to skip these elements.
|
|
2221
|
-
|
|
2267
|
+
//
|
|
2268
|
+
// Implementation note: we use `outerHTML` to swap the element rather than
|
|
2269
|
+
// `setAttribute` per-attribute. Alpine's special attribute names (`@click`,
|
|
2270
|
+
// possibly others starting with `@`) are not valid DOM Names per the XML
|
|
2271
|
+
// production, so `setAttribute('@click', …)` throws InvalidCharacterError.
|
|
2272
|
+
// The HTML parser, on the other hand, is lenient and accepts these names.
|
|
2273
|
+
// Building an HTML string and assigning it via outerHTML round-trips through
|
|
2274
|
+
// the parser and produces an element with all source attributes intact.
|
|
2275
|
+
// Stop Alpine from observing further DOM mutations and flush any pending
|
|
2276
|
+
// effects. Then restore each hydrate target by replacing it with a fresh
|
|
2277
|
+
// element parsed from a source-attribute HTML string. Replacing the element
|
|
2278
|
+
// (rather than mutating attributes in place) detaches it from Alpine's
|
|
2279
|
+
// reactive bindings entirely — the new node has no `_x_*` state, no
|
|
2280
|
+
// effects, and no observers. Alpine's MutationObserver is stopped first
|
|
2281
|
+
// so it can't pick up the new node and re-process it.
|
|
2282
|
+
//
|
|
2283
|
+
// We process snapshots deepest-first so that when an ancestor is rebuilt,
|
|
2284
|
+
// its children have already been replaced with their pristine versions and
|
|
2285
|
+
// are captured (via innerHTML) into the new ancestor.
|
|
2286
|
+
const restoreReport = await page.evaluate(async () => {
|
|
2287
|
+
try { window.Alpine && window.Alpine.flushAndStopDeferringMutations && window.Alpine.flushAndStopDeferringMutations(); } catch (_) {}
|
|
2288
|
+
try { window.Alpine && window.Alpine.stopObservingMutations && window.Alpine.stopObservingMutations(); } catch (_) {}
|
|
2289
|
+
await Promise.resolve();
|
|
2290
|
+
await Promise.resolve();
|
|
2291
|
+
|
|
2222
2292
|
const snapshots = window.__manifestHydrateSnapshots || [];
|
|
2293
|
+
const report = { total: snapshots.length, restored: 0, notFound: 0, errors: [] };
|
|
2294
|
+
|
|
2295
|
+
// Resolve every snapshot to its element, then sort by depth (deepest first).
|
|
2296
|
+
const items = [];
|
|
2223
2297
|
snapshots.forEach(({ id, attrs }) => {
|
|
2224
2298
|
const el = document.querySelector(`[data-manifest-hyd-id="${id}"]`);
|
|
2225
|
-
if (!el) return;
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
Object.entries(attrs).forEach(([name, value]) => {
|
|
2230
|
-
el.setAttribute(name, value);
|
|
2231
|
-
});
|
|
2232
|
-
// Marker so downstream stripping (xfor processing, dynamic-binding strip,
|
|
2233
|
-
// component pre-render marking, etc.) skips this element.
|
|
2234
|
-
el.setAttribute('data-prerender-hydrate', '1');
|
|
2299
|
+
if (!el) { report.notFound++; return; }
|
|
2300
|
+
let depth = 0;
|
|
2301
|
+
for (let p = el.parentNode; p; p = p.parentNode) depth++;
|
|
2302
|
+
items.push({ id, el, attrs, depth });
|
|
2235
2303
|
});
|
|
2304
|
+
items.sort((a, b) => b.depth - a.depth);
|
|
2305
|
+
|
|
2306
|
+
const voidEls = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
|
|
2307
|
+
const escAttr = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/"/g, '"');
|
|
2308
|
+
|
|
2309
|
+
items.forEach(({ id, attrs }) => {
|
|
2310
|
+
// Re-resolve the element by id every iteration: ancestors that were
|
|
2311
|
+
// already rebuilt will have re-parsed their children, so previous
|
|
2312
|
+
// references are stale.
|
|
2313
|
+
const el = document.querySelector(`[data-manifest-hyd-id="${id}"]`);
|
|
2314
|
+
if (!el || !el.parentNode) { report.errors.push({ id, msg: 'lost reference' }); return; }
|
|
2315
|
+
const tag = el.tagName.toLowerCase();
|
|
2316
|
+
const attrString = Object.entries(attrs)
|
|
2317
|
+
.map(([name, value]) => `${name}="${escAttr(value)}"`)
|
|
2318
|
+
.join(' ');
|
|
2319
|
+
const innerHTML = voidEls.has(tag) ? '' : el.innerHTML;
|
|
2320
|
+
const newHTML = voidEls.has(tag)
|
|
2321
|
+
? `<${tag} ${attrString} data-prerender-hydrate="1">`
|
|
2322
|
+
: `<${tag} ${attrString} data-prerender-hydrate="1">${innerHTML}</${tag}>`;
|
|
2323
|
+
// Parse via a temporary container so we can use replaceChild (more
|
|
2324
|
+
// reliable than outerHTML in nested-replace scenarios).
|
|
2325
|
+
const tmp = document.createElement(el.parentNode.tagName === 'TR' ? 'tr' : 'div');
|
|
2326
|
+
tmp.innerHTML = newHTML;
|
|
2327
|
+
const parsed = tmp.firstElementChild;
|
|
2328
|
+
if (!parsed) { report.errors.push({ id, msg: 'parse failed' }); return; }
|
|
2329
|
+
try {
|
|
2330
|
+
el.parentNode.replaceChild(parsed, el);
|
|
2331
|
+
report.restored++;
|
|
2332
|
+
} catch (e) {
|
|
2333
|
+
report.errors.push({ id, tag, msg: String(e && e.message || e) });
|
|
2334
|
+
}
|
|
2335
|
+
});
|
|
2336
|
+
|
|
2337
|
+
return report;
|
|
2236
2338
|
});
|
|
2339
|
+
if (config.debugPrerender) {
|
|
2340
|
+
pushDebug({ path: displayPath, stage: 'hydrate-restore', metrics: restoreReport });
|
|
2341
|
+
}
|
|
2237
2342
|
|
|
2238
2343
|
// x-for lists: keep static lists in the HTML for SEO; collapse only dynamic lists so Alpine re-renders.
|
|
2239
2344
|
// Explicit: data-prerender="dynamic"|"skip". Inferred: x-for uses $search/$query,
|
|
@@ -2619,19 +2724,34 @@ async function runPrerender(config) {
|
|
|
2619
2724
|
}
|
|
2620
2725
|
}
|
|
2621
2726
|
|
|
2622
|
-
// Phase 1: Puppeteer — render base paths, cache raw DOM for substitution
|
|
2727
|
+
// Phase 1: Puppeteer — render base paths, cache raw DOM for substitution.
|
|
2728
|
+
// Any failures (e.g. transient navigation timeouts) are retried up to
|
|
2729
|
+
// `maxRetries` times with a short backoff before being reported as fatal.
|
|
2623
2730
|
try {
|
|
2624
2731
|
let index = 0;
|
|
2625
2732
|
async function worker() {
|
|
2626
2733
|
while (true) {
|
|
2627
2734
|
const i = index++;
|
|
2628
2735
|
if (i >= puppeteerPaths.length) return;
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2736
|
+
const pathSeg = puppeteerPaths[i];
|
|
2737
|
+
let attempt = 0;
|
|
2738
|
+
while (true) {
|
|
2739
|
+
const failureCountBefore = failedPaths.length;
|
|
2740
|
+
await processPath(pathSeg, i, {
|
|
2741
|
+
onRawHtml: (seg, html) => {
|
|
2742
|
+
// Cache raw DOM snapshot for locale variant generation (NOT_FOUND_PATH excluded)
|
|
2743
|
+
if (seg !== NOT_FOUND_PATH) baseHtmlCache.set(seg || '', html);
|
|
2744
|
+
},
|
|
2745
|
+
});
|
|
2746
|
+
if (failedPaths.length === failureCountBefore) break; // success
|
|
2747
|
+
if (attempt >= maxRetries) break; // out of retries — leave the failure recorded
|
|
2748
|
+
// Pop the failure record and retry after a short backoff.
|
|
2749
|
+
failedPaths.pop();
|
|
2750
|
+
attempt++;
|
|
2751
|
+
const displayPath = pathSeg === '' ? '/' : (pathSeg === NOT_FOUND_PATH ? '/__prerender_404__' : '/' + pathSeg);
|
|
2752
|
+
process.stderr.write(`prerender: retrying ${displayPath} (attempt ${attempt + 1}/${maxRetries + 1})\n`);
|
|
2753
|
+
await new Promise((r) => setTimeout(r, 500 * attempt));
|
|
2754
|
+
}
|
|
2635
2755
|
}
|
|
2636
2756
|
}
|
|
2637
2757
|
await Promise.all(
|