mnfst-render 0.5.44 → 0.5.45
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 +178 -149
- package/package.json +2 -2
package/manifest.render.mjs
CHANGED
|
@@ -27,9 +27,7 @@ async function importFromProject(moduleName) {
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
async function flushAlpineEffects(page) {
|
|
30
|
-
// Alpine's scheduler can swallow a nextTick callback
|
|
31
|
-
// already-flushed job is dropped), so this wait must be bounded or a page
|
|
32
|
-
// hangs idle until the per-page timeout kills it.
|
|
30
|
+
// Bounded: Alpine's scheduler can swallow a nextTick callback
|
|
33
31
|
await page
|
|
34
32
|
.evaluate(() => {
|
|
35
33
|
return new Promise((resolve) => {
|
|
@@ -172,8 +170,7 @@ async function waitForManifestRenderReady(page, { allLocales, currentLocale, tim
|
|
|
172
170
|
|
|
173
171
|
// 6. Run component swapping explicitly so components tied to this route render
|
|
174
172
|
// and trigger any $x accesses that start on-demand data loads.
|
|
175
|
-
// Bounded: a stranded component
|
|
176
|
-
// pipeline past the render-ready timeout (step 7 would never run).
|
|
173
|
+
// Bounded: a stranded component load must not hang the pipeline.
|
|
177
174
|
if (window.ManifestComponentsSwapping?.processAll) {
|
|
178
175
|
try {
|
|
179
176
|
await Promise.race([
|
|
@@ -232,16 +229,16 @@ export function isTransientFrameError(message) {
|
|
|
232
229
|
}
|
|
233
230
|
|
|
234
231
|
/**
|
|
235
|
-
* Coordinates
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
* or a stuck browser swap logs and continues instead of hanging the run.
|
|
241
|
-
* Exported for unit tests.
|
|
232
|
+
* Coordinates render workers around a shared browser swapped every `every`
|
|
233
|
+
* completed paths. A recycle pauses new slots, drains in-flight ones, swaps,
|
|
234
|
+
* resumes. Forced recycles are coalesced per browser generation and rate
|
|
235
|
+
* limited by `forcedCooldown` completed paths, unless `healthy()` reports a
|
|
236
|
+
* dead browser. Exported for unit tests.
|
|
242
237
|
*/
|
|
243
238
|
export function createRecycleGate({
|
|
244
239
|
every = 40,
|
|
240
|
+
forcedCooldown = Math.max(1, Math.ceil(every / 4)),
|
|
241
|
+
healthy = () => true,
|
|
245
242
|
recycle = async () => {},
|
|
246
243
|
drainTimeoutMs = 60000,
|
|
247
244
|
recycleTimeoutMs = 180000,
|
|
@@ -249,60 +246,70 @@ export function createRecycleGate({
|
|
|
249
246
|
} = {}) {
|
|
250
247
|
let active = 0;
|
|
251
248
|
let pages = 0;
|
|
249
|
+
let sinceForced = Infinity;
|
|
250
|
+
let forced = false;
|
|
252
251
|
let paused = false;
|
|
252
|
+
let generation = 0;
|
|
253
|
+
let recycles = 0;
|
|
254
|
+
let forcedRecycles = 0;
|
|
253
255
|
const zeroWaiters = [];
|
|
254
256
|
const resumeWaiters = [];
|
|
255
257
|
const wake = (list) => { for (const resolve of list.splice(0)) resolve(); };
|
|
258
|
+
const due = () => every > 0 && (pages >= every || (forced && (sinceForced >= forcedCooldown || !healthy())));
|
|
256
259
|
|
|
257
260
|
return {
|
|
258
261
|
get active() { return active; },
|
|
259
262
|
get pages() { return pages; },
|
|
260
263
|
get paused() { return paused; },
|
|
261
|
-
|
|
264
|
+
get generation() { return generation; },
|
|
265
|
+
get forced() { return forced; },
|
|
266
|
+
get recycles() { return recycles; },
|
|
267
|
+
get forcedRecycles() { return forcedRecycles; },
|
|
268
|
+
/** Claim a browser slot; resolves with the browser generation. */
|
|
262
269
|
async acquire() {
|
|
263
270
|
while (paused) await new Promise((resolve) => { resumeWaiters.push(resolve); });
|
|
264
271
|
active++;
|
|
272
|
+
return generation;
|
|
265
273
|
},
|
|
266
274
|
release() {
|
|
267
275
|
active = Math.max(0, active - 1);
|
|
268
276
|
if (active === 0) wake(zeroWaiters);
|
|
269
277
|
},
|
|
270
|
-
/** Count
|
|
271
|
-
countPage() { pages++; },
|
|
272
|
-
/**
|
|
273
|
-
requestRecycle() {
|
|
274
|
-
|
|
278
|
+
/** Count one completed path. */
|
|
279
|
+
countPage() { pages++; sinceForced++; },
|
|
280
|
+
/** Ask for a fresh browser; ignored if `gen` is already replaced. */
|
|
281
|
+
requestRecycle(gen) {
|
|
282
|
+
if (every <= 0) return false;
|
|
283
|
+
if (gen !== undefined && gen !== generation) return false;
|
|
284
|
+
forced = true;
|
|
285
|
+
return true;
|
|
286
|
+
},
|
|
287
|
+
/** Swap the browser if due. Never throws. */
|
|
275
288
|
async maybeRecycle() {
|
|
276
|
-
if (
|
|
277
|
-
paused = true;
|
|
289
|
+
if (paused || !due()) return false;
|
|
290
|
+
paused = true;
|
|
278
291
|
const processed = pages;
|
|
292
|
+
const isForced = pages < every;
|
|
279
293
|
try {
|
|
280
|
-
|
|
281
|
-
// Never swap the browser under a live page — a per-page caller
|
|
282
|
-
// timeout already bounds how long any page can hold a slot, so
|
|
283
|
-
// waiting here cannot deadlock. drainTimeoutMs is configured to
|
|
284
|
-
// exceed that per-page timeout (see recycleDrainTimeout default in
|
|
285
|
-
// resolveConfig), so hitting it means a page leaked past its own
|
|
286
|
-
// timeout without releasing — a bug, not a normal recycle path.
|
|
287
|
-
// Log it and keep waiting rather than recycle while it's live.
|
|
294
|
+
while (active > 0) {
|
|
288
295
|
const drained = await withDeadline(
|
|
289
296
|
new Promise((resolve) => { zeroWaiters.push(resolve); }),
|
|
290
297
|
drainTimeoutMs
|
|
291
298
|
);
|
|
292
|
-
if (!drained) {
|
|
293
|
-
onLog(`recycle: ${active} page(s) still in flight after ${drainTimeoutMs}ms — waiting (a leaked page never released its slot)`);
|
|
294
|
-
await new Promise((resolve) => { zeroWaiters.push(resolve); });
|
|
295
|
-
}
|
|
299
|
+
if (!drained) onLog(`recycle: ${active} page(s) still in flight after ${drainTimeoutMs}ms — waiting (a leaked page never released its slot)`);
|
|
296
300
|
}
|
|
297
|
-
|
|
301
|
+
recycles++;
|
|
302
|
+
if (isForced) { forcedRecycles++; sinceForced = 0; }
|
|
303
|
+
const swapped = await withDeadline(recycle(processed, { forced: isForced }), recycleTimeoutMs);
|
|
298
304
|
if (!swapped) onLog(`recycle: browser swap exceeded ${recycleTimeoutMs}ms — continuing`);
|
|
299
|
-
pages = 0;
|
|
300
305
|
return swapped;
|
|
301
306
|
} catch (err) {
|
|
302
307
|
onLog(`recycle: failed (${err && err.message ? err.message : err}) — continuing`);
|
|
303
|
-
pages = 0;
|
|
304
308
|
return false;
|
|
305
309
|
} finally {
|
|
310
|
+
pages = 0;
|
|
311
|
+
forced = false;
|
|
312
|
+
generation++;
|
|
306
313
|
paused = false;
|
|
307
314
|
wake(resumeWaiters);
|
|
308
315
|
}
|
|
@@ -310,6 +317,81 @@ export function createRecycleGate({
|
|
|
310
317
|
};
|
|
311
318
|
}
|
|
312
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Render `items` through the gate with per-attempt watchdog and retries.
|
|
322
|
+
* `render(item, index, token)` may set `token.cancel` to tear down a wedged
|
|
323
|
+
* attempt; a timed-out attempt keeps its slot until cancelled/settled
|
|
324
|
+
* (bounded by `abandonGraceMs`) so a swap never lands under it.
|
|
325
|
+
* Exported for unit tests.
|
|
326
|
+
*/
|
|
327
|
+
export async function runRenderQueue({
|
|
328
|
+
items,
|
|
329
|
+
concurrency = 1,
|
|
330
|
+
gate,
|
|
331
|
+
render,
|
|
332
|
+
pageTimeoutMs = 0,
|
|
333
|
+
maxRetries = 2,
|
|
334
|
+
abandonGraceMs = 15000,
|
|
335
|
+
labelOf = (item) => String(item),
|
|
336
|
+
onRetry = () => {},
|
|
337
|
+
onFail = () => {},
|
|
338
|
+
} = {}) {
|
|
339
|
+
let index = 0;
|
|
340
|
+
const failures = [];
|
|
341
|
+
|
|
342
|
+
async function worker() {
|
|
343
|
+
while (true) {
|
|
344
|
+
const i = index++;
|
|
345
|
+
if (i >= items.length) return;
|
|
346
|
+
const item = items[i];
|
|
347
|
+
const label = labelOf(item);
|
|
348
|
+
let attempt = 0;
|
|
349
|
+
while (true) {
|
|
350
|
+
const token = { abandoned: false, failure: null, cancel: null };
|
|
351
|
+
const gen = await gate.acquire();
|
|
352
|
+
let timedOut = false;
|
|
353
|
+
try {
|
|
354
|
+
const run = Promise.resolve().then(() => render(item, i, token));
|
|
355
|
+
const finished = await withDeadline(run, pageTimeoutMs);
|
|
356
|
+
if (!finished) {
|
|
357
|
+
timedOut = true;
|
|
358
|
+
token.abandoned = true;
|
|
359
|
+
token.failure = { path: label, message: `page render exceeded ${pageTimeoutMs}ms` };
|
|
360
|
+
// Hold the slot until the orphan is torn down
|
|
361
|
+
await withDeadline(Promise.resolve().then(() => token.cancel?.()).catch(() => {}), abandonGraceMs);
|
|
362
|
+
await withDeadline(run.catch(() => {}), abandonGraceMs);
|
|
363
|
+
}
|
|
364
|
+
} catch (err) {
|
|
365
|
+
token.failure = { path: label, message: err && err.message ? err.message : String(err) };
|
|
366
|
+
} finally {
|
|
367
|
+
gate.release();
|
|
368
|
+
}
|
|
369
|
+
if (!token.failure) { gate.countPage(); break; }
|
|
370
|
+
if (attempt >= maxRetries) {
|
|
371
|
+
failures.push(token.failure);
|
|
372
|
+
onFail(token.failure, failures.length);
|
|
373
|
+
gate.countPage();
|
|
374
|
+
gate.requestRecycle(gen);
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
const immediate = isTransientFrameError(token.failure.message);
|
|
378
|
+
// Suspect browser: fresh one before the retry
|
|
379
|
+
if (timedOut || (!immediate && attempt + 1 >= Math.ceil(maxRetries / 2))) {
|
|
380
|
+
gate.requestRecycle(gen);
|
|
381
|
+
await gate.maybeRecycle();
|
|
382
|
+
}
|
|
383
|
+
attempt++;
|
|
384
|
+
onRetry(token.failure, attempt, immediate);
|
|
385
|
+
if (!immediate) await new Promise((r) => setTimeout(r, 500 * attempt));
|
|
386
|
+
}
|
|
387
|
+
await gate.maybeRecycle();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length || 1) }, () => worker()));
|
|
392
|
+
return failures;
|
|
393
|
+
}
|
|
394
|
+
|
|
313
395
|
// --- Config ------------------------------------------------------------------
|
|
314
396
|
|
|
315
397
|
function parseArgs() {
|
|
@@ -3621,17 +3703,13 @@ async function runPrerender(config) {
|
|
|
3621
3703
|
console.error(' npm i -D puppeteer-core @sparticuz/chromium');
|
|
3622
3704
|
process.exit(1);
|
|
3623
3705
|
}
|
|
3624
|
-
//
|
|
3625
|
-
// hang forever on macOS (frames never produced), which wedges og
|
|
3626
|
-
// snapshots and, through the shared browser, stalls sibling pages.
|
|
3706
|
+
// Headless shell: new headless can hang captureScreenshot on macOS
|
|
3627
3707
|
return await puppeteer.default.launch({
|
|
3628
3708
|
headless: 'shell',
|
|
3629
3709
|
args: [
|
|
3630
3710
|
'--no-sandbox',
|
|
3631
3711
|
'--disable-setuid-sandbox',
|
|
3632
|
-
//
|
|
3633
|
-
// backgrounded tab stops producing frames and firing rAF/timers, so
|
|
3634
|
-
// its captureScreenshot (og snapshots) and settle waits hang forever.
|
|
3712
|
+
// Background tabs must keep producing frames and timers
|
|
3635
3713
|
'--disable-background-timer-throttling',
|
|
3636
3714
|
'--disable-backgrounding-occluded-windows',
|
|
3637
3715
|
'--disable-renderer-backgrounding',
|
|
@@ -3695,9 +3773,10 @@ async function runPrerender(config) {
|
|
|
3695
3773
|
every: browserRecycleEvery,
|
|
3696
3774
|
drainTimeoutMs,
|
|
3697
3775
|
recycleTimeoutMs,
|
|
3776
|
+
healthy: () => !!browser && browser.connected !== false,
|
|
3698
3777
|
onLog: (message) => process.stderr.write(`prerender: ${message}\n`),
|
|
3699
|
-
recycle: async (processed) => {
|
|
3700
|
-
process.stdout.write(`prerender: recycling browser (processed ${processed} pages)\n`);
|
|
3778
|
+
recycle: async (processed, { forced } = {}) => {
|
|
3779
|
+
process.stdout.write(`prerender: recycling browser (${forced ? `forced after ${processed}` : `processed ${processed}`} pages)\n`);
|
|
3701
3780
|
await closeBrowser(browser);
|
|
3702
3781
|
browser = await launchBrowserBounded();
|
|
3703
3782
|
},
|
|
@@ -3855,6 +3934,7 @@ async function runPrerender(config) {
|
|
|
3855
3934
|
// No recycle can run while the caller holds a gate slot, so `browser` is
|
|
3856
3935
|
// live for the whole call.
|
|
3857
3936
|
const page = await browser.newPage();
|
|
3937
|
+
if (attempt) attempt.cancel = () => page.close();
|
|
3858
3938
|
// Render at a typical desktop viewport so layouts dependent on viewport
|
|
3859
3939
|
// width (responsive flex/grid, container queries, media queries) settle
|
|
3860
3940
|
// into their desktop variant. Without this the headless default (often
|
|
@@ -4094,8 +4174,7 @@ async function runPrerender(config) {
|
|
|
4094
4174
|
// small ones settle in well under 1 second.
|
|
4095
4175
|
await page.waitForNetworkIdle({ idleTime: 1000, timeout: 8000 }).catch(() => { });
|
|
4096
4176
|
|
|
4097
|
-
// DOM stability after network idle
|
|
4098
|
-
// continuously must not reset the quiet window forever.
|
|
4177
|
+
// DOM stability after network idle (hard-capped).
|
|
4099
4178
|
await page.evaluate(() => {
|
|
4100
4179
|
return new Promise((resolve) => {
|
|
4101
4180
|
const observer = new MutationObserver(() => {
|
|
@@ -4136,8 +4215,7 @@ async function runPrerender(config) {
|
|
|
4136
4215
|
const ogImageHandled = !!config.seo.meta?.image
|
|
4137
4216
|
|| await page.evaluate(() => !!document.head.querySelector('meta[property="og:image"]'));
|
|
4138
4217
|
if (!ogImageHandled) {
|
|
4139
|
-
// Bounded: a CDP call
|
|
4140
|
-
// image, not the whole render (the meta fallback image covers it).
|
|
4218
|
+
// Bounded: a hung CDP call costs only this page's og image
|
|
4141
4219
|
earlySnapshotUrl = await Promise.race([
|
|
4142
4220
|
takeOgSnapshot(page, config.output, is404 ? '__404__' : pathSeg, globalAssetSig, ogCacheDir),
|
|
4143
4221
|
new Promise((resolve) => setTimeout(() => resolve(null), 30000)),
|
|
@@ -4315,6 +4393,25 @@ async function runPrerender(config) {
|
|
|
4315
4393
|
await page.evaluate(() => {
|
|
4316
4394
|
document.querySelectorAll('[x-markdown]').forEach((el) => {
|
|
4317
4395
|
if (!el.textContent.trim() && !el.innerHTML.trim()) return;
|
|
4396
|
+
// Only FILE-BACKED markdown is safe to freeze: its content is baked and
|
|
4397
|
+
// the runtime would only re-fetch and re-render the same thing (with an
|
|
4398
|
+
// opacity flash, or a wipe when a route-keyed source starts empty). The
|
|
4399
|
+
// plugin stamps those with data-mnfst-md-src before fetching. An inline
|
|
4400
|
+
// reactive value (x-markdown="note") must KEEP its binding — stripping
|
|
4401
|
+
// it froze live-updating markdown mirrors on prerendered pages.
|
|
4402
|
+
const src = el.getAttribute('data-mnfst-md-src');
|
|
4403
|
+
const literal = /^\s*(['"`]).*\1\s*$/.test(el.getAttribute('x-markdown') || '');
|
|
4404
|
+
if (!src && !literal) {
|
|
4405
|
+
// Reactive: keep the binding, still clean the plugin's leftover styles.
|
|
4406
|
+
const s = el.getAttribute('style') || '';
|
|
4407
|
+
if (s) {
|
|
4408
|
+
const c = s.replace(/\bopacity\s*:\s*0(?:\.\d+)?\s*;?/gi, '')
|
|
4409
|
+
.replace(/\btransition\s*:\s*opacity[^;]*;?/gi, '')
|
|
4410
|
+
.replace(/;\s*;/g, ';').replace(/^\s*;\s*|\s*;\s*$/g, '').trim();
|
|
4411
|
+
if (c) el.setAttribute('style', c); else el.removeAttribute('style');
|
|
4412
|
+
}
|
|
4413
|
+
return;
|
|
4414
|
+
}
|
|
4318
4415
|
el.removeAttribute('x-markdown');
|
|
4319
4416
|
// Clean up opacity-0 + transition inline styles the plugin left behind.
|
|
4320
4417
|
const style = el.getAttribute('style') || '';
|
|
@@ -4744,14 +4841,17 @@ async function runPrerender(config) {
|
|
|
4744
4841
|
if (name.startsWith(':')) boundAttr = name.slice(1);
|
|
4745
4842
|
else if (name.startsWith('x-bind:')) boundAttr = name.slice('x-bind:'.length);
|
|
4746
4843
|
if (boundAttr) {
|
|
4844
|
+
// Removing :foo triggers Alpine's binding teardown, which
|
|
4845
|
+
// restores the bound attribute to its pre-binding value
|
|
4846
|
+
// (empty for clones whose template had no static class).
|
|
4847
|
+
// Snapshot the eval'd value and re-set it after removal so
|
|
4848
|
+
// the baked attribute survives the unbind. A binding whose
|
|
4849
|
+
// value evaluated to null (no attribute) is removed all the
|
|
4850
|
+
// same — left in place it re-evaluates at runtime outside
|
|
4851
|
+
// the loop scope and throws on every render.
|
|
4747
4852
|
const concrete = node.getAttribute(boundAttr);
|
|
4853
|
+
node.removeAttribute(name);
|
|
4748
4854
|
if (concrete != null && String(concrete).trim() !== '') {
|
|
4749
|
-
// Removing :foo triggers Alpine's binding teardown, which
|
|
4750
|
-
// restores the bound attribute to its pre-binding value
|
|
4751
|
-
// (empty for clones whose template had no static class).
|
|
4752
|
-
// Snapshot the eval'd value and re-set it after removal so
|
|
4753
|
-
// the baked attribute survives the unbind.
|
|
4754
|
-
node.removeAttribute(name);
|
|
4755
4855
|
node.setAttribute(boundAttr, concrete);
|
|
4756
4856
|
}
|
|
4757
4857
|
continue;
|
|
@@ -5190,103 +5290,32 @@ async function runPrerender(config) {
|
|
|
5190
5290
|
}
|
|
5191
5291
|
}
|
|
5192
5292
|
|
|
5193
|
-
// Phase 1: Puppeteer — render base paths, cache raw DOM for substitution
|
|
5194
|
-
// Any failures (e.g. transient navigation timeouts) are retried up to
|
|
5195
|
-
// `maxRetries` times with a short backoff before being reported as fatal.
|
|
5196
|
-
//
|
|
5197
|
-
// Browser recycling: after every `browserRecycleEvery` successful pages the
|
|
5198
|
-
// gate pauses new work, drains the in-flight pages, swaps the browser and
|
|
5199
|
-
// resumes. Workers hold a gate slot only while they touch the browser.
|
|
5200
|
-
//
|
|
5201
|
-
// A failure whose message matches a transient Puppeteer/browser-recycle race
|
|
5202
|
-
// (detached frame, destroyed execution context) is requeued immediately —
|
|
5203
|
-
// no backoff — since waiting doesn't help a race and each attempt already
|
|
5204
|
-
// gets its own fresh `pageTimeoutMs` budget.
|
|
5293
|
+
// Phase 1: Puppeteer — render base paths, cache raw DOM for substitution
|
|
5205
5294
|
async function renderPuppeteerPaths(paths) {
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
}),
|
|
5231
|
-
pageTimeoutMs
|
|
5232
|
-
);
|
|
5233
|
-
if (!finished) {
|
|
5234
|
-
attemptToken.abandoned = true;
|
|
5235
|
-
attemptToken.failure = { path: displayPath, message: `page render exceeded ${pageTimeoutMs}ms` };
|
|
5236
|
-
// A timed-out page is presumed to have wedged the browser (e.g.
|
|
5237
|
-
// a hung page.evaluate on a dead Chrome process) — force a
|
|
5238
|
-
// recycle once this path is done, even if a later retry
|
|
5239
|
-
// "succeeds" by reusing the same possibly-poisoned browser.
|
|
5240
|
-
recycleGate.requestRecycle();
|
|
5241
|
-
}
|
|
5242
|
-
} catch (err) {
|
|
5243
|
-
// Unexpected exception escaped processPath (e.g. browser died mid-call).
|
|
5244
|
-
attemptToken.failure = {
|
|
5245
|
-
path: displayPath,
|
|
5246
|
-
message: err && err.message ? err.message : String(err),
|
|
5247
|
-
};
|
|
5248
|
-
} finally {
|
|
5249
|
-
recycleGate.release();
|
|
5250
|
-
}
|
|
5251
|
-
if (!attemptToken.failure) {
|
|
5252
|
-
recycleGate.countPage();
|
|
5253
|
-
break; // success
|
|
5254
|
-
}
|
|
5255
|
-
if (attempt >= maxRetries) {
|
|
5256
|
-
// Exhausted retries — likely an unstable browser (e.g. cascading
|
|
5257
|
-
// "detached Frame" errors). Force the next path onto a fresh one.
|
|
5258
|
-
failedPaths.push(attemptToken.failure);
|
|
5259
|
-
if (failedPaths.length <= 10) {
|
|
5260
|
-
process.stderr.write(`prerender: failed ${displayPath}: ${attemptToken.failure.message}\n`);
|
|
5261
|
-
}
|
|
5262
|
-
recycleGate.countPage();
|
|
5263
|
-
recycleGate.requestRecycle();
|
|
5264
|
-
break;
|
|
5265
|
-
}
|
|
5266
|
-
const immediateRequeue = isTransientFrameError(attemptToken.failure.message);
|
|
5267
|
-
// Halfway through retries with no success → preemptively recycle the
|
|
5268
|
-
// browser before the next attempt. This unblocks cascading frame
|
|
5269
|
-
// failures where the browser process needs a fresh start. Skipped
|
|
5270
|
-
// for a transient frame error — that's likely already a recycle
|
|
5271
|
-
// race, not a signal to force another one.
|
|
5272
|
-
if (!immediateRequeue && attempt + 1 >= Math.ceil(maxRetries / 2) && recycleGate.pages > 0) {
|
|
5273
|
-
recycleGate.requestRecycle();
|
|
5274
|
-
await recycleGate.maybeRecycle();
|
|
5275
|
-
}
|
|
5276
|
-
attempt++;
|
|
5277
|
-
process.stderr.write(
|
|
5278
|
-
`prerender: retrying ${displayPath} (${attemptToken.failure.message}) (attempt ${attempt + 1}/${maxRetries + 1})${immediateRequeue ? ' — immediate requeue' : ''}\n`
|
|
5279
|
-
);
|
|
5280
|
-
if (!immediateRequeue) await new Promise((r) => setTimeout(r, 500 * attempt));
|
|
5281
|
-
}
|
|
5282
|
-
// Attempt recycle after each completed path (only one worker performs
|
|
5283
|
-
// it; the rest block in acquire()).
|
|
5284
|
-
await recycleGate.maybeRecycle();
|
|
5285
|
-
}
|
|
5286
|
-
}
|
|
5287
|
-
await Promise.all(
|
|
5288
|
-
Array.from({ length: Math.min(concurrency, total || 1) }, () => worker())
|
|
5289
|
-
);
|
|
5295
|
+
const failures = await runRenderQueue({
|
|
5296
|
+
items: paths,
|
|
5297
|
+
concurrency,
|
|
5298
|
+
gate: recycleGate,
|
|
5299
|
+
pageTimeoutMs,
|
|
5300
|
+
maxRetries,
|
|
5301
|
+
labelOf: (seg) => (seg === '' ? '/' : (seg === NOT_FOUND_PATH ? '/__prerender_404__' : '/' + seg)),
|
|
5302
|
+
render: (pathSeg, i, attempt) => processPath(pathSeg, i, {
|
|
5303
|
+
attempt,
|
|
5304
|
+
onRawHtml: (seg, html) => {
|
|
5305
|
+
if (seg !== NOT_FOUND_PATH) baseHtmlCache.set(seg || '', html);
|
|
5306
|
+
},
|
|
5307
|
+
onLocaleDependentContent: promoteLocaleVariants,
|
|
5308
|
+
}),
|
|
5309
|
+
onRetry: (failure, attempt, immediate) => {
|
|
5310
|
+
process.stderr.write(
|
|
5311
|
+
`prerender: retrying ${failure.path} (${failure.message}) (attempt ${attempt + 1}/${maxRetries + 1})${immediate ? ' — immediate requeue' : ''}\n`
|
|
5312
|
+
);
|
|
5313
|
+
},
|
|
5314
|
+
onFail: (failure, count) => {
|
|
5315
|
+
if (count <= 10) process.stderr.write(`prerender: failed ${failure.path}: ${failure.message}\n`);
|
|
5316
|
+
},
|
|
5317
|
+
});
|
|
5318
|
+
failedPaths.push(...failures);
|
|
5290
5319
|
}
|
|
5291
5320
|
|
|
5292
5321
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mnfst-render",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.45",
|
|
4
4
|
"description": "Render Manifest sites to static HTML for SEO",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"license": "MIT",
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
|
35
|
-
"url": "git+https://github.com/
|
|
35
|
+
"url": "git+https://github.com/Manifest-X/Manifest.git",
|
|
36
36
|
"directory": "packages/render"
|
|
37
37
|
}
|
|
38
38
|
}
|