mnfst-render 0.5.4 → 0.5.6
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 +72 -17
- package/package.json +1 -1
package/manifest.render.mjs
CHANGED
|
@@ -1356,7 +1356,7 @@ function generateLocaleVariantHtml({
|
|
|
1356
1356
|
html = stripEmptyInlineMaskStyles(html);
|
|
1357
1357
|
html = stripResolvedXIconDirectives(html);
|
|
1358
1358
|
// markPrerenderedManifestComponents must run BEFORE stripPrerenderHydrateMarkers so it can
|
|
1359
|
-
// detect data-
|
|
1359
|
+
// detect data-hydrate markers and skip components inside hydrate islands.
|
|
1360
1360
|
html = markPrerenderedManifestComponents(html);
|
|
1361
1361
|
|
|
1362
1362
|
const fileSegments = pathToFileSegments(pathSeg ? '/' + pathSeg : '/');
|
|
@@ -1787,6 +1787,11 @@ async function runPrerender(config) {
|
|
|
1787
1787
|
const browserRecycleEvery = Math.max(0, pre.browserRecycleEvery ?? 40);
|
|
1788
1788
|
let pagesSinceRecycle = 0;
|
|
1789
1789
|
const recycleLock = { busy: false };
|
|
1790
|
+
// Workers block on this promise before touching `browser`. While a recycle
|
|
1791
|
+
// is in progress it's a pending promise; once the new browser is up it
|
|
1792
|
+
// resolves and workers can proceed. This prevents "browser not ready"
|
|
1793
|
+
// errors from racing retries during recycle.
|
|
1794
|
+
let browserReadyPromise = Promise.resolve();
|
|
1790
1795
|
const pathTotal = pathList.length;
|
|
1791
1796
|
const failedPaths = [];
|
|
1792
1797
|
const debugRows = [];
|
|
@@ -1878,11 +1883,11 @@ async function runPrerender(config) {
|
|
|
1878
1883
|
: defaultLocale || 'en'
|
|
1879
1884
|
: defaultLocale || 'en';
|
|
1880
1885
|
|
|
1881
|
-
//
|
|
1882
|
-
//
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
+
// Wait for any in-progress browser recycle to complete before touching
|
|
1887
|
+
// `browser`. This transparently handles the window between the old
|
|
1888
|
+
// browser being closed and the new one being launched — workers block
|
|
1889
|
+
// here instead of throwing "browser not ready".
|
|
1890
|
+
await browserReadyPromise;
|
|
1886
1891
|
const page = await browser.newPage();
|
|
1887
1892
|
try {
|
|
1888
1893
|
// Align <html lang> with the URL being prerendered before any app script runs.
|
|
@@ -2234,13 +2239,36 @@ async function runPrerender(config) {
|
|
|
2234
2239
|
});
|
|
2235
2240
|
|
|
2236
2241
|
// Strip x-markdown from elements that already have baked content.
|
|
2237
|
-
// The markdown plugin hides elements with opacity:0 on init, then re-fetches
|
|
2238
|
-
// For prerendered pages the content is already baked —
|
|
2239
|
-
// runtime plugin from re-processing (and
|
|
2242
|
+
// The markdown plugin hides elements with opacity:0 on init, then re-fetches
|
|
2243
|
+
// and re-renders. For prerendered pages the content is already baked —
|
|
2244
|
+
// removing x-markdown prevents the runtime plugin from re-processing (and
|
|
2245
|
+
// temporarily hiding) the static content.
|
|
2246
|
+
//
|
|
2247
|
+
// We ALSO clear any leftover `opacity: 0` inline style the plugin set
|
|
2248
|
+
// before/while rendering. On dynamic expressions that initially evaluate
|
|
2249
|
+
// empty (e.g. article content keyed off `$route` before `$x.articles` has
|
|
2250
|
+
// loaded), the plugin sets opacity to 0 and may never restore it to 1 if
|
|
2251
|
+
// the effect re-fires with an empty value. The end state in the
|
|
2252
|
+
// serialized HTML has rendered content but opacity:0 — invisible in
|
|
2253
|
+
// production. Since we're also removing x-markdown (so the runtime
|
|
2254
|
+
// plugin doesn't re-hide the element), leaving the inline style would
|
|
2255
|
+
// permanently hide authored content.
|
|
2240
2256
|
await page.evaluate(() => {
|
|
2241
2257
|
document.querySelectorAll('[x-markdown]').forEach((el) => {
|
|
2242
2258
|
if (!el.textContent.trim() && !el.innerHTML.trim()) return;
|
|
2243
2259
|
el.removeAttribute('x-markdown');
|
|
2260
|
+
// Clean up opacity-0 + transition inline styles the plugin left behind.
|
|
2261
|
+
const style = el.getAttribute('style') || '';
|
|
2262
|
+
if (style) {
|
|
2263
|
+
const cleaned = style
|
|
2264
|
+
.replace(/\bopacity\s*:\s*0(?:\.\d+)?\s*;?/gi, '')
|
|
2265
|
+
.replace(/\btransition\s*:\s*opacity[^;]*;?/gi, '')
|
|
2266
|
+
.replace(/;\s*;/g, ';')
|
|
2267
|
+
.replace(/^\s*;\s*|\s*;\s*$/g, '')
|
|
2268
|
+
.trim();
|
|
2269
|
+
if (cleaned) el.setAttribute('style', cleaned);
|
|
2270
|
+
else el.removeAttribute('style');
|
|
2271
|
+
}
|
|
2244
2272
|
});
|
|
2245
2273
|
});
|
|
2246
2274
|
|
|
@@ -2399,7 +2427,7 @@ async function runPrerender(config) {
|
|
|
2399
2427
|
// $url, $auth, or iterates over getter names (filtered*, results, searchResults). See docs prerender + local.data.
|
|
2400
2428
|
await page.evaluate(() => {
|
|
2401
2429
|
document.querySelectorAll('template[x-for]').forEach((tpl) => {
|
|
2402
|
-
if (tpl.hasAttribute('data-
|
|
2430
|
+
if (tpl.hasAttribute('data-hydrate') || tpl.closest('[data-hydrate]')) {
|
|
2403
2431
|
tpl.removeAttribute('data-prerender-collapsed');
|
|
2404
2432
|
tpl.removeAttribute('data-prerender-static-generated');
|
|
2405
2433
|
return;
|
|
@@ -2480,7 +2508,7 @@ async function runPrerender(config) {
|
|
|
2480
2508
|
await page.evaluate(() => {
|
|
2481
2509
|
const loopVarRx = /^\s*(?:\(\s*([A-Za-z_$][\w$]*)(?:\s*,\s*([A-Za-z_$][\w$]*))?\s*\)|([A-Za-z_$][\w$]*))\s+in\s+/;
|
|
2482
2510
|
document.querySelectorAll('template[x-for][data-prerender-static-generated="1"]').forEach((tpl) => {
|
|
2483
|
-
if (tpl.hasAttribute('data-
|
|
2511
|
+
if (tpl.hasAttribute('data-hydrate') || tpl.closest('[data-hydrate]')) return;
|
|
2484
2512
|
const xFor = (tpl.getAttribute('x-for') || '').trim();
|
|
2485
2513
|
const m = xFor.match(loopVarRx);
|
|
2486
2514
|
const itemVar = m ? (m[1] || m[3] || '') : '';
|
|
@@ -2494,7 +2522,7 @@ async function runPrerender(config) {
|
|
|
2494
2522
|
// Only process clones that contain data-hydrate descendants
|
|
2495
2523
|
if (
|
|
2496
2524
|
!n.hasAttribute('x-data') &&
|
|
2497
|
-
(n.hasAttribute('data-
|
|
2525
|
+
(n.hasAttribute('data-hydrate') || n.querySelector('[data-hydrate]'))
|
|
2498
2526
|
) {
|
|
2499
2527
|
try {
|
|
2500
2528
|
const A = window.Alpine;
|
|
@@ -2536,7 +2564,7 @@ async function runPrerender(config) {
|
|
|
2536
2564
|
const nodes = [el, ...Array.from(el.querySelectorAll('*'))];
|
|
2537
2565
|
for (const node of nodes) {
|
|
2538
2566
|
// Skip elements inside data-hydrate islands — their bindings must remain live
|
|
2539
|
-
if (node.hasAttribute('data-
|
|
2567
|
+
if (node.hasAttribute('data-hydrate') || node.closest('[data-hydrate]')) continue;
|
|
2540
2568
|
const attrs = node.attributes ? Array.from(node.attributes) : [];
|
|
2541
2569
|
for (const attr of attrs) {
|
|
2542
2570
|
if (!bindingAttrRegex.test(attr.name)) continue;
|
|
@@ -2574,7 +2602,7 @@ async function runPrerender(config) {
|
|
|
2574
2602
|
};
|
|
2575
2603
|
|
|
2576
2604
|
document.querySelectorAll('template[x-for]').forEach((tpl) => {
|
|
2577
|
-
if (tpl.hasAttribute('data-
|
|
2605
|
+
if (tpl.hasAttribute('data-hydrate') || tpl.closest('[data-hydrate]')) return;
|
|
2578
2606
|
const xFor = (tpl.getAttribute('x-for') || '').trim();
|
|
2579
2607
|
const m = xFor.match(loopVarRegex);
|
|
2580
2608
|
const itemVar = m ? (m[1] || m[3] || '') : '';
|
|
@@ -2603,7 +2631,7 @@ async function runPrerender(config) {
|
|
|
2603
2631
|
const runBatch = typeof A?.mutateDom === 'function' ? (fn) => A.mutateDom(fn) : (fn) => fn();
|
|
2604
2632
|
runBatch(() => {
|
|
2605
2633
|
document.querySelectorAll('template[x-for][data-prerender-static-generated="1"]').forEach((tpl) => {
|
|
2606
|
-
if (tpl.hasAttribute('data-
|
|
2634
|
+
if (tpl.hasAttribute('data-hydrate') || tpl.closest('[data-hydrate]')) return;
|
|
2607
2635
|
const parent = tpl.parentNode;
|
|
2608
2636
|
if (!parent) {
|
|
2609
2637
|
tpl.remove();
|
|
@@ -2816,14 +2844,23 @@ async function runPrerender(config) {
|
|
|
2816
2844
|
if (pagesSinceRecycle < browserRecycleEvery) return;
|
|
2817
2845
|
if (recycleLock.busy) return;
|
|
2818
2846
|
recycleLock.busy = true;
|
|
2847
|
+
// Wait for all in-flight workers to finish their current page BEFORE
|
|
2848
|
+
// we gate `browserReadyPromise`, so workers already mid-processPath
|
|
2849
|
+
// don't deadlock awaiting a promise we haven't yet started.
|
|
2850
|
+
await waitUntilZero();
|
|
2851
|
+
// Now gate newPage() calls from any worker that enters processPath
|
|
2852
|
+
// after this point.
|
|
2853
|
+
let resolveReady;
|
|
2854
|
+
browserReadyPromise = new Promise((r) => { resolveReady = r; });
|
|
2819
2855
|
try {
|
|
2820
|
-
// Wait for all in-flight workers to finish their current page.
|
|
2821
|
-
await waitUntilZero();
|
|
2822
2856
|
process.stdout.write(`prerender: recycling browser (processed ${pagesSinceRecycle} pages)\n`);
|
|
2823
2857
|
try { await browser.close(); } catch (_) {}
|
|
2824
2858
|
browser = await launchBrowser();
|
|
2825
2859
|
pagesSinceRecycle = 0;
|
|
2826
2860
|
} finally {
|
|
2861
|
+
// Release the gate first so any waiting workers can proceed, then
|
|
2862
|
+
// clear the recycle lock so the outer while loop stops pausing.
|
|
2863
|
+
try { resolveReady(); } catch (_) {}
|
|
2827
2864
|
recycleLock.busy = false;
|
|
2828
2865
|
const r = recycleGate.resume;
|
|
2829
2866
|
recycleGate.resume = null;
|
|
@@ -2835,12 +2872,19 @@ async function runPrerender(config) {
|
|
|
2835
2872
|
while (true) {
|
|
2836
2873
|
// Pause if a recycle is underway.
|
|
2837
2874
|
if (recycleLock.busy) await waitForResume();
|
|
2875
|
+
// Also wait for any pending browser readiness (e.g. another worker
|
|
2876
|
+
// started a recycle while we were processing).
|
|
2877
|
+
await browserReadyPromise;
|
|
2838
2878
|
|
|
2839
2879
|
const i = index++;
|
|
2840
2880
|
if (i >= puppeteerPaths.length) return;
|
|
2841
2881
|
const pathSeg = puppeteerPaths[i];
|
|
2842
2882
|
let attempt = 0;
|
|
2843
2883
|
while (true) {
|
|
2884
|
+
// Re-check recycle state at the start of every retry iteration.
|
|
2885
|
+
if (recycleLock.busy) await waitForResume();
|
|
2886
|
+
await browserReadyPromise;
|
|
2887
|
+
|
|
2844
2888
|
const failureCountBefore = failedPaths.length;
|
|
2845
2889
|
activeWorkers++;
|
|
2846
2890
|
try {
|
|
@@ -2849,6 +2893,17 @@ async function runPrerender(config) {
|
|
|
2849
2893
|
if (seg !== NOT_FOUND_PATH) baseHtmlCache.set(seg || '', html);
|
|
2850
2894
|
},
|
|
2851
2895
|
});
|
|
2896
|
+
} catch (err) {
|
|
2897
|
+
// Unexpected exception escaped processPath (e.g. browser died
|
|
2898
|
+
// mid-call). Record as a failure so the retry logic can handle
|
|
2899
|
+
// it gracefully instead of tearing down the whole worker.
|
|
2900
|
+
failedPaths.push({
|
|
2901
|
+
path: pathSeg === '' ? '/' : '/' + pathSeg,
|
|
2902
|
+
message: err && err.message ? err.message : String(err),
|
|
2903
|
+
});
|
|
2904
|
+
if (failedPaths.length <= 10) {
|
|
2905
|
+
process.stderr.write(`prerender: worker exception on ${pathSeg || '/'}: ${failedPaths[failedPaths.length - 1].message}\n`);
|
|
2906
|
+
}
|
|
2852
2907
|
} finally {
|
|
2853
2908
|
activeWorkers--;
|
|
2854
2909
|
if (activeWorkers === 0 && recycleGate.waitForZero) {
|