mnfst-render 0.5.38 → 0.5.39
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 +204 -118
- package/package.json +1 -1
package/manifest.render.mjs
CHANGED
|
@@ -189,6 +189,96 @@ async function waitForManifestRenderReady(page, { allLocales, currentLocale, tim
|
|
|
189
189
|
// intentional and benign — the DOM is still captured.
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// --- Browser recycle gate ----------------------------------------------------
|
|
193
|
+
|
|
194
|
+
/** Bound a promise: resolves true if it settles first, false on timeout. */
|
|
195
|
+
function withDeadline(promise, ms) {
|
|
196
|
+
if (!(ms > 0)) return Promise.resolve(promise).then(() => true);
|
|
197
|
+
let timer;
|
|
198
|
+
const settled = Promise.resolve(promise).then(
|
|
199
|
+
() => { clearTimeout(timer); return true; },
|
|
200
|
+
(err) => { clearTimeout(timer); throw err; }
|
|
201
|
+
);
|
|
202
|
+
return Promise.race([
|
|
203
|
+
settled,
|
|
204
|
+
new Promise((resolve) => {
|
|
205
|
+
timer = setTimeout(() => resolve(false), ms);
|
|
206
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
207
|
+
}),
|
|
208
|
+
]);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Coordinates N render workers around a shared browser that is swapped out
|
|
213
|
+
* every `every` pages. Workers hold a slot (acquire/release) for exactly as
|
|
214
|
+
* long as they touch the browser; a recycle pauses new slots first, then
|
|
215
|
+
* drains the in-flight ones, so no worker can ever be blocked on the gate
|
|
216
|
+
* while it is still counted as active. Every wait is bounded: a stuck page
|
|
217
|
+
* or a stuck browser swap logs and continues instead of hanging the run.
|
|
218
|
+
* Exported for unit tests.
|
|
219
|
+
*/
|
|
220
|
+
export function createRecycleGate({
|
|
221
|
+
every = 40,
|
|
222
|
+
recycle = async () => {},
|
|
223
|
+
drainTimeoutMs = 60000,
|
|
224
|
+
recycleTimeoutMs = 180000,
|
|
225
|
+
onLog = () => {},
|
|
226
|
+
} = {}) {
|
|
227
|
+
let active = 0;
|
|
228
|
+
let pages = 0;
|
|
229
|
+
let paused = false;
|
|
230
|
+
const zeroWaiters = [];
|
|
231
|
+
const resumeWaiters = [];
|
|
232
|
+
const wake = (list) => { for (const resolve of list.splice(0)) resolve(); };
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
get active() { return active; },
|
|
236
|
+
get pages() { return pages; },
|
|
237
|
+
get paused() { return paused; },
|
|
238
|
+
/** Claim a browser slot; blocks while a recycle is pending or running. */
|
|
239
|
+
async acquire() {
|
|
240
|
+
while (paused) await new Promise((resolve) => { resumeWaiters.push(resolve); });
|
|
241
|
+
active++;
|
|
242
|
+
},
|
|
243
|
+
release() {
|
|
244
|
+
active = Math.max(0, active - 1);
|
|
245
|
+
if (active === 0) wake(zeroWaiters);
|
|
246
|
+
},
|
|
247
|
+
/** Count a page against the recycle threshold. */
|
|
248
|
+
countPage() { pages++; },
|
|
249
|
+
/** Make the next maybeRecycle() fire (unstable browser after retries). */
|
|
250
|
+
requestRecycle() { if (every > 0) pages = Math.max(pages, every); },
|
|
251
|
+
/** Swap the browser if the threshold is reached. Never throws. */
|
|
252
|
+
async maybeRecycle() {
|
|
253
|
+
if (every <= 0 || paused || pages < every) return false;
|
|
254
|
+
paused = true; // no new slots from here on — active can only fall
|
|
255
|
+
const processed = pages;
|
|
256
|
+
try {
|
|
257
|
+
if (active > 0) {
|
|
258
|
+
const drained = await withDeadline(
|
|
259
|
+
new Promise((resolve) => { zeroWaiters.push(resolve); }),
|
|
260
|
+
drainTimeoutMs
|
|
261
|
+
);
|
|
262
|
+
if (!drained) {
|
|
263
|
+
onLog(`recycle: ${active} page(s) still in flight after ${drainTimeoutMs}ms — recycling anyway`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const swapped = await withDeadline(recycle(processed), recycleTimeoutMs);
|
|
267
|
+
if (!swapped) onLog(`recycle: browser swap exceeded ${recycleTimeoutMs}ms — continuing`);
|
|
268
|
+
pages = 0;
|
|
269
|
+
return swapped;
|
|
270
|
+
} catch (err) {
|
|
271
|
+
onLog(`recycle: failed (${err && err.message ? err.message : err}) — continuing`);
|
|
272
|
+
pages = 0;
|
|
273
|
+
return false;
|
|
274
|
+
} finally {
|
|
275
|
+
paused = false;
|
|
276
|
+
wake(resumeWaiters);
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
192
282
|
// --- Config ------------------------------------------------------------------
|
|
193
283
|
|
|
194
284
|
function parseArgs() {
|
|
@@ -1010,15 +1100,16 @@ function buildRootAssetPath(routerBasePath, filename) {
|
|
|
1010
1100
|
return '/' + path.replace(/\/{2,}/g, '/');
|
|
1011
1101
|
}
|
|
1012
1102
|
|
|
1013
|
-
/** Inject stylesheet link with root-absolute href (avoids ../ resolving under locale segments like /en/page/).
|
|
1014
|
-
|
|
1103
|
+
/** Inject stylesheet link with root-absolute href (avoids ../ resolving under locale segments like /en/page/).
|
|
1104
|
+
* `attrs` (e.g. `data-mnfst-utilities`) lets the runtime utilities plugin find and skip classes this sheet covers. */
|
|
1105
|
+
function postProcessInjectStylesheetLink(outputDir, filename, routerBasePath, attrs = '') {
|
|
1015
1106
|
const cssPath = join(outputDir, filename);
|
|
1016
1107
|
if (!existsSync(cssPath)) return;
|
|
1017
1108
|
const stat = statSync(cssPath);
|
|
1018
1109
|
if (stat.size === 0) return;
|
|
1019
1110
|
|
|
1020
1111
|
const href = buildRootAssetPath(routerBasePath, filename);
|
|
1021
|
-
const tag = `<link rel="stylesheet" href="${href}">`;
|
|
1112
|
+
const tag = `<link rel="stylesheet" href="${href}"${attrs ? ` ${attrs}` : ''}>`;
|
|
1022
1113
|
const files = walkHtmlFiles(outputDir);
|
|
1023
1114
|
for (const file of files) {
|
|
1024
1115
|
let html = readFileSync(file, 'utf8');
|
|
@@ -2344,8 +2435,10 @@ async function injectMetaInDom(page, ctx) {
|
|
|
2344
2435
|
if (existingTypes.has(type)) continue;
|
|
2345
2436
|
if (def === false) continue;
|
|
2346
2437
|
if (type === 'BreadcrumbList' && def === true) {
|
|
2347
|
-
// Auto-derive from URL path segments
|
|
2438
|
+
// Auto-derive from URL path segments; skip crumbs whose URL has no
|
|
2439
|
+
// rendered page (avoids linking soft-404 intermediate paths).
|
|
2348
2440
|
const parts = location.pathname.split('/').filter(Boolean);
|
|
2441
|
+
const rendered = Array.isArray(ctx.renderedPaths) ? new Set(ctx.renderedPaths) : null;
|
|
2349
2442
|
const items = [{
|
|
2350
2443
|
'@type': 'ListItem',
|
|
2351
2444
|
position: 1,
|
|
@@ -2353,11 +2446,13 @@ async function injectMetaInDom(page, ctx) {
|
|
|
2353
2446
|
item: liveBase ? liveBase + '/' : '/',
|
|
2354
2447
|
}];
|
|
2355
2448
|
parts.forEach((seg, i) => {
|
|
2449
|
+
const crumbPath = parts.slice(0, i + 1).join('/');
|
|
2450
|
+
if (rendered && !rendered.has(crumbPath)) return;
|
|
2356
2451
|
items.push({
|
|
2357
2452
|
'@type': 'ListItem',
|
|
2358
|
-
position:
|
|
2453
|
+
position: items.length + 1,
|
|
2359
2454
|
name: seg.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
2360
|
-
item: liveBase ? `${liveBase}/${
|
|
2455
|
+
item: liveBase ? `${liveBase}/${crumbPath}` : '/' + crumbPath,
|
|
2361
2456
|
});
|
|
2362
2457
|
});
|
|
2363
2458
|
blocks.push({ '@context': 'https://schema.org', '@type': 'BreadcrumbList', itemListElement: items });
|
|
@@ -3259,13 +3354,49 @@ async function runPrerender(config) {
|
|
|
3259
3354
|
// Recycle the browser every N processed pages to bound resource growth.
|
|
3260
3355
|
// Configurable via manifest.prerender.browserRecycleEvery.
|
|
3261
3356
|
const browserRecycleEvery = Math.max(0, pre.browserRecycleEvery ?? 40);
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3357
|
+
// Bounds on the recycle handshake and on a single page render. A stuck
|
|
3358
|
+
// page must never pin the whole run: each is capped, logged and stepped over.
|
|
3359
|
+
const drainTimeoutMs = Math.max(0, pre.recycleDrainTimeout ?? 60000);
|
|
3360
|
+
const recycleTimeoutMs = Math.max(0, pre.recycleTimeout ?? 180000);
|
|
3361
|
+
const pageTimeoutMs = Math.max(0, pre.pageTimeout ?? Math.max(120000, (config.wait ?? 30000) * 4));
|
|
3362
|
+
|
|
3363
|
+
// Close the browser without hanging: SIGKILL the process if close stalls.
|
|
3364
|
+
async function closeBrowser(target) {
|
|
3365
|
+
if (!target) return;
|
|
3366
|
+
const proc = typeof target.process === 'function' ? target.process() : null;
|
|
3367
|
+
const closed = await withDeadline(Promise.resolve(target.close()).catch(() => {}), 15000);
|
|
3368
|
+
if (!closed) {
|
|
3369
|
+
process.stderr.write('prerender: browser close timed out — killing the process\n');
|
|
3370
|
+
try { proc?.kill('SIGKILL'); } catch (_) {}
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
// Launch with a deadline and one retry; a hung launch would otherwise stall
|
|
3375
|
+
// every worker behind the recycle gate.
|
|
3376
|
+
async function launchBrowserBounded() {
|
|
3377
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
3378
|
+
let launched = null;
|
|
3379
|
+
const ok = await withDeadline(
|
|
3380
|
+
launchBrowser().then((b) => { launched = b; }),
|
|
3381
|
+
60000
|
|
3382
|
+
);
|
|
3383
|
+
if (ok && launched) return launched;
|
|
3384
|
+
process.stderr.write('prerender: browser launch timed out — retrying\n');
|
|
3385
|
+
}
|
|
3386
|
+
throw new Error('browser launch timed out');
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
const recycleGate = createRecycleGate({
|
|
3390
|
+
every: browserRecycleEvery,
|
|
3391
|
+
drainTimeoutMs,
|
|
3392
|
+
recycleTimeoutMs,
|
|
3393
|
+
onLog: (message) => process.stderr.write(`prerender: ${message}\n`),
|
|
3394
|
+
recycle: async (processed) => {
|
|
3395
|
+
process.stdout.write(`prerender: recycling browser (processed ${processed} pages)\n`);
|
|
3396
|
+
await closeBrowser(browser);
|
|
3397
|
+
browser = await launchBrowserBounded();
|
|
3398
|
+
},
|
|
3399
|
+
});
|
|
3269
3400
|
const pathTotal = pathList.length;
|
|
3270
3401
|
const failedPaths = [];
|
|
3271
3402
|
const debugRows = [];
|
|
@@ -3354,7 +3485,7 @@ async function runPrerender(config) {
|
|
|
3354
3485
|
debugRows.push(row);
|
|
3355
3486
|
}
|
|
3356
3487
|
|
|
3357
|
-
async function processPath(pathSeg, pathIndex, { onRawHtml } = {}) {
|
|
3488
|
+
async function processPath(pathSeg, pathIndex, { onRawHtml, attempt } = {}) {
|
|
3358
3489
|
const is404 = pathSeg === NOT_FOUND_PATH;
|
|
3359
3490
|
const pathname = is404 ? `/${NOT_FOUND_PATH}` : (pathSeg ? `/${pathSeg}` : '/');
|
|
3360
3491
|
const displayPath = pathSeg === '' ? '/' : pathname;
|
|
@@ -3370,11 +3501,8 @@ async function runPrerender(config) {
|
|
|
3370
3501
|
: defaultLocale || 'en'
|
|
3371
3502
|
: defaultLocale || 'en';
|
|
3372
3503
|
|
|
3373
|
-
//
|
|
3374
|
-
//
|
|
3375
|
-
// browser being closed and the new one being launched — workers block
|
|
3376
|
-
// here instead of throwing "browser not ready".
|
|
3377
|
-
await browserReadyPromise;
|
|
3504
|
+
// No recycle can run while the caller holds a gate slot, so `browser` is
|
|
3505
|
+
// live for the whole call.
|
|
3378
3506
|
const page = await browser.newPage();
|
|
3379
3507
|
// Render at a typical desktop viewport so layouts dependent on viewport
|
|
3380
3508
|
// width (responsive flex/grid, container queries, media queries) settle
|
|
@@ -3388,6 +3516,9 @@ async function runPrerender(config) {
|
|
|
3388
3516
|
// initializeDataSourcesPlugin picks locale from document.documentElement.lang first; a mismatch
|
|
3389
3517
|
// (e.g. headless default vs /en/...) leaves $x.* empty while x-route sections still render.
|
|
3390
3518
|
await page.evaluateOnNewDocument((locale) => {
|
|
3519
|
+
// Render pass: the defer plugin keeps closed containers eager so the
|
|
3520
|
+
// snapshot carries their markup (hydration stashes it again).
|
|
3521
|
+
window.__manifestRender = true;
|
|
3391
3522
|
const apply = () => {
|
|
3392
3523
|
try {
|
|
3393
3524
|
if (locale && typeof locale === 'string') document.documentElement.lang = locale;
|
|
@@ -4532,6 +4663,7 @@ async function runPrerender(config) {
|
|
|
4532
4663
|
liveUrl: (config.liveUrl || '').replace(/\/$/, ''),
|
|
4533
4664
|
pathSeg: is404 ? '__404__' : pathSeg,
|
|
4534
4665
|
snapshotUrl: earlySnapshotUrl,
|
|
4666
|
+
renderedPaths: pathList.filter((p) => p !== NOT_FOUND_PATH),
|
|
4535
4667
|
});
|
|
4536
4668
|
|
|
4537
4669
|
let html = await page.evaluate(() => document.documentElement.outerHTML);
|
|
@@ -4623,6 +4755,7 @@ async function runPrerender(config) {
|
|
|
4623
4755
|
// (Hydration contract was already injected into the raw HTML before
|
|
4624
4756
|
// the Node.js post-processing pipeline ran, so it's already present.)
|
|
4625
4757
|
html = ensureDoctype(html);
|
|
4758
|
+
if (attempt?.abandoned) return; // a retry owns the output now
|
|
4626
4759
|
mkdirSync(outDir, { recursive: true });
|
|
4627
4760
|
writeFileSync(outFile, html, 'utf8');
|
|
4628
4761
|
pushDebug({
|
|
@@ -4633,13 +4766,13 @@ async function runPrerender(config) {
|
|
|
4633
4766
|
hasXForTemplate: html.includes('template x-for') || html.includes('template[x-for]'),
|
|
4634
4767
|
});
|
|
4635
4768
|
} catch (err) {
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
if (
|
|
4641
|
-
|
|
4642
|
-
}
|
|
4769
|
+
if (attempt?.abandoned) return;
|
|
4770
|
+
const message = err && err.message ? err.message : String(err);
|
|
4771
|
+
// The worker owns retries and reporting; a shared array can't be popped
|
|
4772
|
+
// safely while other workers push to it.
|
|
4773
|
+
if (attempt) { attempt.failure = { path: displayPath, message }; return; }
|
|
4774
|
+
failedPaths.push({ path: displayPath, message });
|
|
4775
|
+
process.stderr.write(`prerender: failed ${displayPath}: ${message}\n`);
|
|
4643
4776
|
} finally {
|
|
4644
4777
|
try { await page.close(); } catch (_) { /* page may be gone if browser died */ }
|
|
4645
4778
|
}
|
|
@@ -4649,131 +4782,84 @@ async function runPrerender(config) {
|
|
|
4649
4782
|
// Any failures (e.g. transient navigation timeouts) are retried up to
|
|
4650
4783
|
// `maxRetries` times with a short backoff before being reported as fatal.
|
|
4651
4784
|
//
|
|
4652
|
-
// Browser recycling: after every `browserRecycleEvery` successful pages
|
|
4653
|
-
//
|
|
4654
|
-
//
|
|
4785
|
+
// Browser recycling: after every `browserRecycleEvery` successful pages the
|
|
4786
|
+
// gate pauses new work, drains the in-flight pages, swaps the browser and
|
|
4787
|
+
// resumes. Workers hold a gate slot only while they touch the browser.
|
|
4655
4788
|
try {
|
|
4656
4789
|
let index = 0;
|
|
4657
|
-
let activeWorkers = 0;
|
|
4658
|
-
const recycleGate = { resume: null, waitForZero: null };
|
|
4659
|
-
|
|
4660
|
-
const waitUntilZero = () => new Promise((resolve) => {
|
|
4661
|
-
if (activeWorkers === 0) return resolve();
|
|
4662
|
-
recycleGate.waitForZero = resolve;
|
|
4663
|
-
});
|
|
4664
|
-
const waitForResume = () => new Promise((resolve) => {
|
|
4665
|
-
if (!recycleLock.busy) return resolve();
|
|
4666
|
-
const prev = recycleGate.resume;
|
|
4667
|
-
recycleGate.resume = () => { if (prev) prev(); resolve(); };
|
|
4668
|
-
});
|
|
4669
|
-
|
|
4670
|
-
const maybeRecycleBrowser = async () => {
|
|
4671
|
-
if (browserRecycleEvery <= 0) return;
|
|
4672
|
-
if (pagesSinceRecycle < browserRecycleEvery) return;
|
|
4673
|
-
if (recycleLock.busy) return;
|
|
4674
|
-
recycleLock.busy = true;
|
|
4675
|
-
// Wait for all in-flight workers to finish their current page BEFORE
|
|
4676
|
-
// we gate `browserReadyPromise`, so workers already mid-processPath
|
|
4677
|
-
// don't deadlock awaiting a promise we haven't yet started.
|
|
4678
|
-
await waitUntilZero();
|
|
4679
|
-
// Now gate newPage() calls from any worker that enters processPath
|
|
4680
|
-
// after this point.
|
|
4681
|
-
let resolveReady;
|
|
4682
|
-
browserReadyPromise = new Promise((r) => { resolveReady = r; });
|
|
4683
|
-
try {
|
|
4684
|
-
process.stdout.write(`prerender: recycling browser (processed ${pagesSinceRecycle} pages)\n`);
|
|
4685
|
-
try { await browser.close(); } catch (_) {}
|
|
4686
|
-
browser = await launchBrowser();
|
|
4687
|
-
pagesSinceRecycle = 0;
|
|
4688
|
-
} finally {
|
|
4689
|
-
// Release the gate first so any waiting workers can proceed, then
|
|
4690
|
-
// clear the recycle lock so the outer while loop stops pausing.
|
|
4691
|
-
try { resolveReady(); } catch (_) {}
|
|
4692
|
-
recycleLock.busy = false;
|
|
4693
|
-
const r = recycleGate.resume;
|
|
4694
|
-
recycleGate.resume = null;
|
|
4695
|
-
if (r) r();
|
|
4696
|
-
}
|
|
4697
|
-
};
|
|
4698
4790
|
|
|
4699
4791
|
async function worker() {
|
|
4700
4792
|
while (true) {
|
|
4701
|
-
// Pause if a recycle is underway.
|
|
4702
|
-
if (recycleLock.busy) await waitForResume();
|
|
4703
|
-
// Also wait for any pending browser readiness (e.g. another worker
|
|
4704
|
-
// started a recycle while we were processing).
|
|
4705
|
-
await browserReadyPromise;
|
|
4706
|
-
|
|
4707
4793
|
const i = index++;
|
|
4708
4794
|
if (i >= puppeteerPaths.length) return;
|
|
4709
4795
|
const pathSeg = puppeteerPaths[i];
|
|
4796
|
+
const displayPath = pathSeg === '' ? '/' : (pathSeg === NOT_FOUND_PATH ? '/__prerender_404__' : '/' + pathSeg);
|
|
4710
4797
|
let attempt = 0;
|
|
4711
4798
|
while (true) {
|
|
4712
|
-
//
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
const failureCountBefore = failedPaths.length;
|
|
4717
|
-
activeWorkers++;
|
|
4799
|
+
// Per-attempt result: `failedPaths` is shared, so a retry must never
|
|
4800
|
+
// read or pop entries another worker pushed.
|
|
4801
|
+
const attemptToken = { abandoned: false, failure: null };
|
|
4802
|
+
await recycleGate.acquire();
|
|
4718
4803
|
try {
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4804
|
+
// Watchdog: a wedged renderer never returns, so cap the attempt.
|
|
4805
|
+
const finished = await withDeadline(
|
|
4806
|
+
processPath(pathSeg, i, {
|
|
4807
|
+
attempt: attemptToken,
|
|
4808
|
+
onRawHtml: (seg, html) => {
|
|
4809
|
+
if (seg !== NOT_FOUND_PATH) baseHtmlCache.set(seg || '', html);
|
|
4810
|
+
},
|
|
4811
|
+
}),
|
|
4812
|
+
pageTimeoutMs
|
|
4813
|
+
);
|
|
4814
|
+
if (!finished) {
|
|
4815
|
+
attemptToken.abandoned = true;
|
|
4816
|
+
attemptToken.failure = { path: displayPath, message: `page render exceeded ${pageTimeoutMs}ms` };
|
|
4817
|
+
}
|
|
4724
4818
|
} catch (err) {
|
|
4725
|
-
// Unexpected exception escaped processPath (e.g. browser died
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
failedPaths.push({
|
|
4729
|
-
path: pathSeg === '' ? '/' : '/' + pathSeg,
|
|
4819
|
+
// Unexpected exception escaped processPath (e.g. browser died mid-call).
|
|
4820
|
+
attemptToken.failure = {
|
|
4821
|
+
path: displayPath,
|
|
4730
4822
|
message: err && err.message ? err.message : String(err),
|
|
4731
|
-
}
|
|
4732
|
-
if (failedPaths.length <= 10) {
|
|
4733
|
-
process.stderr.write(`prerender: worker exception on ${pathSeg || '/'}: ${failedPaths[failedPaths.length - 1].message}\n`);
|
|
4734
|
-
}
|
|
4823
|
+
};
|
|
4735
4824
|
} finally {
|
|
4736
|
-
|
|
4737
|
-
if (activeWorkers === 0 && recycleGate.waitForZero) {
|
|
4738
|
-
const z = recycleGate.waitForZero;
|
|
4739
|
-
recycleGate.waitForZero = null;
|
|
4740
|
-
z();
|
|
4741
|
-
}
|
|
4825
|
+
recycleGate.release();
|
|
4742
4826
|
}
|
|
4743
|
-
if (
|
|
4744
|
-
|
|
4827
|
+
if (!attemptToken.failure) {
|
|
4828
|
+
recycleGate.countPage();
|
|
4745
4829
|
break; // success
|
|
4746
4830
|
}
|
|
4747
4831
|
if (attempt >= maxRetries) {
|
|
4748
4832
|
// Exhausted retries — likely an unstable browser (e.g. cascading
|
|
4749
|
-
// "detached Frame" errors). Force
|
|
4750
|
-
|
|
4751
|
-
|
|
4833
|
+
// "detached Frame" errors). Force the next path onto a fresh one.
|
|
4834
|
+
failedPaths.push(attemptToken.failure);
|
|
4835
|
+
if (failedPaths.length <= 10) {
|
|
4836
|
+
process.stderr.write(`prerender: failed ${displayPath}: ${attemptToken.failure.message}\n`);
|
|
4837
|
+
}
|
|
4838
|
+
recycleGate.countPage();
|
|
4839
|
+
recycleGate.requestRecycle();
|
|
4752
4840
|
break;
|
|
4753
4841
|
}
|
|
4754
4842
|
// Halfway through retries with no success → preemptively recycle the
|
|
4755
4843
|
// browser before the next attempt. This unblocks cascading frame
|
|
4756
4844
|
// failures where the browser process needs a fresh start.
|
|
4757
|
-
if (attempt + 1 >= Math.ceil(maxRetries / 2) &&
|
|
4758
|
-
|
|
4759
|
-
await
|
|
4845
|
+
if (attempt + 1 >= Math.ceil(maxRetries / 2) && recycleGate.pages > 0) {
|
|
4846
|
+
recycleGate.requestRecycle();
|
|
4847
|
+
await recycleGate.maybeRecycle();
|
|
4760
4848
|
}
|
|
4761
|
-
failedPaths.pop();
|
|
4762
4849
|
attempt++;
|
|
4763
|
-
|
|
4764
|
-
process.stderr.write(`prerender: retrying ${displayPath} (attempt ${attempt + 1}/${maxRetries + 1})\n`);
|
|
4850
|
+
process.stderr.write(`prerender: retrying ${displayPath} (${attemptToken.failure.message}) (attempt ${attempt + 1}/${maxRetries + 1})\n`);
|
|
4765
4851
|
await new Promise((r) => setTimeout(r, 500 * attempt));
|
|
4766
4852
|
}
|
|
4767
|
-
// Attempt recycle after each completed path (only one worker
|
|
4768
|
-
//
|
|
4769
|
-
await
|
|
4853
|
+
// Attempt recycle after each completed path (only one worker performs
|
|
4854
|
+
// it; the rest block in acquire()).
|
|
4855
|
+
await recycleGate.maybeRecycle();
|
|
4770
4856
|
}
|
|
4771
4857
|
}
|
|
4772
4858
|
await Promise.all(
|
|
4773
4859
|
Array.from({ length: Math.min(concurrency, puppeteerPaths.length || 1) }, () => worker())
|
|
4774
4860
|
);
|
|
4775
4861
|
} finally {
|
|
4776
|
-
|
|
4862
|
+
await closeBrowser(browser);
|
|
4777
4863
|
}
|
|
4778
4864
|
|
|
4779
4865
|
// Phase 2: Node.js — generate locale variants via text substitution
|
|
@@ -4842,7 +4928,7 @@ async function runPrerender(config) {
|
|
|
4842
4928
|
} else {
|
|
4843
4929
|
writeFileSync(join(outputResolved, 'prerender.utilities.css'), `${utilMerged}\n`, 'utf8');
|
|
4844
4930
|
process.stdout.write('prerender: wrote prerender.utilities.css (Manifest custom utilities)\n');
|
|
4845
|
-
postProcessInjectStylesheetLink(outputResolved, 'prerender.utilities.css', routerBasePath || '');
|
|
4931
|
+
postProcessInjectStylesheetLink(outputResolved, 'prerender.utilities.css', routerBasePath || '', 'data-mnfst-utilities');
|
|
4846
4932
|
}
|
|
4847
4933
|
}
|
|
4848
4934
|
}
|