mnfst-render 0.5.43 → 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.
Files changed (2) hide show
  1. package/manifest.render.mjs +202 -144
  2. package/package.json +2 -2
@@ -27,20 +27,23 @@ async function importFromProject(moduleName) {
27
27
 
28
28
 
29
29
  async function flushAlpineEffects(page) {
30
+ // Bounded: Alpine's scheduler can swallow a nextTick callback
30
31
  await page
31
32
  .evaluate(() => {
32
33
  return new Promise((resolve) => {
34
+ const cap = setTimeout(resolve, 3000);
35
+ const done = () => { clearTimeout(cap); resolve(); };
33
36
  try {
34
37
  if (typeof Alpine !== 'undefined' && typeof Alpine.nextTick === 'function') {
35
38
  Alpine.nextTick(() => {
36
- if (typeof Alpine.nextTick === 'function') Alpine.nextTick(resolve);
37
- else resolve();
39
+ if (typeof Alpine.nextTick === 'function') Alpine.nextTick(done);
40
+ else done();
38
41
  });
39
42
  } else {
40
- queueMicrotask(resolve);
43
+ queueMicrotask(done);
41
44
  }
42
45
  } catch {
43
- resolve();
46
+ done();
44
47
  }
45
48
  });
46
49
  })
@@ -149,7 +152,10 @@ async function waitForManifestRenderReady(page, { allLocales, currentLocale, tim
149
152
  try {
150
153
  const cfg = window.ManifestDataConfig;
151
154
  const main = window.ManifestDataMain;
152
- const manifest = await cfg?.ensureManifest?.();
155
+ const manifest = await Promise.race([
156
+ Promise.resolve(cfg?.ensureManifest?.()),
157
+ new Promise((resolve) => setTimeout(resolve, 5000)),
158
+ ]);
153
159
  if (manifest?.data && typeof main?.loadDataSource === 'function') {
154
160
  const isAppwrite = cfg.isAppwriteCollection;
155
161
  for (const [name, source] of Object.entries(manifest.data)) {
@@ -164,9 +170,13 @@ async function waitForManifestRenderReady(page, { allLocales, currentLocale, tim
164
170
 
165
171
  // 6. Run component swapping explicitly so components tied to this route render
166
172
  // and trigger any $x accesses that start on-demand data loads.
173
+ // Bounded: a stranded component load must not hang the pipeline.
167
174
  if (window.ManifestComponentsSwapping?.processAll) {
168
175
  try {
169
- await window.ManifestComponentsSwapping.processAll(normalizedPath);
176
+ await Promise.race([
177
+ window.ManifestComponentsSwapping.processAll(normalizedPath),
178
+ new Promise((resolve) => setTimeout(resolve, ms)),
179
+ ]);
170
180
  } catch (e) {
171
181
  return { ok: false, reason: 'processAll-error', message: String(e?.message || e) };
172
182
  }
@@ -219,16 +229,16 @@ export function isTransientFrameError(message) {
219
229
  }
220
230
 
221
231
  /**
222
- * Coordinates N render workers around a shared browser that is swapped out
223
- * every `every` pages. Workers hold a slot (acquire/release) for exactly as
224
- * long as they touch the browser; a recycle pauses new slots first, then
225
- * drains the in-flight ones, so no worker can ever be blocked on the gate
226
- * while it is still counted as active. Every wait is bounded: a stuck page
227
- * or a stuck browser swap logs and continues instead of hanging the run.
228
- * 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.
229
237
  */
230
238
  export function createRecycleGate({
231
239
  every = 40,
240
+ forcedCooldown = Math.max(1, Math.ceil(every / 4)),
241
+ healthy = () => true,
232
242
  recycle = async () => {},
233
243
  drainTimeoutMs = 60000,
234
244
  recycleTimeoutMs = 180000,
@@ -236,60 +246,70 @@ export function createRecycleGate({
236
246
  } = {}) {
237
247
  let active = 0;
238
248
  let pages = 0;
249
+ let sinceForced = Infinity;
250
+ let forced = false;
239
251
  let paused = false;
252
+ let generation = 0;
253
+ let recycles = 0;
254
+ let forcedRecycles = 0;
240
255
  const zeroWaiters = [];
241
256
  const resumeWaiters = [];
242
257
  const wake = (list) => { for (const resolve of list.splice(0)) resolve(); };
258
+ const due = () => every > 0 && (pages >= every || (forced && (sinceForced >= forcedCooldown || !healthy())));
243
259
 
244
260
  return {
245
261
  get active() { return active; },
246
262
  get pages() { return pages; },
247
263
  get paused() { return paused; },
248
- /** Claim a browser slot; blocks while a recycle is pending or running. */
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. */
249
269
  async acquire() {
250
270
  while (paused) await new Promise((resolve) => { resumeWaiters.push(resolve); });
251
271
  active++;
272
+ return generation;
252
273
  },
253
274
  release() {
254
275
  active = Math.max(0, active - 1);
255
276
  if (active === 0) wake(zeroWaiters);
256
277
  },
257
- /** Count a page against the recycle threshold. */
258
- countPage() { pages++; },
259
- /** Make the next maybeRecycle() fire (unstable browser after retries). */
260
- requestRecycle() { if (every > 0) pages = Math.max(pages, every); },
261
- /** Swap the browser if the threshold is reached. Never throws. */
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. */
262
288
  async maybeRecycle() {
263
- if (every <= 0 || paused || pages < every) return false;
264
- paused = true; // no new slots from here on — active can only fall
289
+ if (paused || !due()) return false;
290
+ paused = true;
265
291
  const processed = pages;
292
+ const isForced = pages < every;
266
293
  try {
267
- if (active > 0) {
268
- // Never swap the browser under a live page — a per-page caller
269
- // timeout already bounds how long any page can hold a slot, so
270
- // waiting here cannot deadlock. drainTimeoutMs is configured to
271
- // exceed that per-page timeout (see recycleDrainTimeout default in
272
- // resolveConfig), so hitting it means a page leaked past its own
273
- // timeout without releasing — a bug, not a normal recycle path.
274
- // Log it and keep waiting rather than recycle while it's live.
294
+ while (active > 0) {
275
295
  const drained = await withDeadline(
276
296
  new Promise((resolve) => { zeroWaiters.push(resolve); }),
277
297
  drainTimeoutMs
278
298
  );
279
- if (!drained) {
280
- onLog(`recycle: ${active} page(s) still in flight after ${drainTimeoutMs}ms — waiting (a leaked page never released its slot)`);
281
- await new Promise((resolve) => { zeroWaiters.push(resolve); });
282
- }
299
+ if (!drained) onLog(`recycle: ${active} page(s) still in flight after ${drainTimeoutMs}ms — waiting (a leaked page never released its slot)`);
283
300
  }
284
- const swapped = await withDeadline(recycle(processed), recycleTimeoutMs);
301
+ recycles++;
302
+ if (isForced) { forcedRecycles++; sinceForced = 0; }
303
+ const swapped = await withDeadline(recycle(processed, { forced: isForced }), recycleTimeoutMs);
285
304
  if (!swapped) onLog(`recycle: browser swap exceeded ${recycleTimeoutMs}ms — continuing`);
286
- pages = 0;
287
305
  return swapped;
288
306
  } catch (err) {
289
307
  onLog(`recycle: failed (${err && err.message ? err.message : err}) — continuing`);
290
- pages = 0;
291
308
  return false;
292
309
  } finally {
310
+ pages = 0;
311
+ forced = false;
312
+ generation++;
293
313
  paused = false;
294
314
  wake(resumeWaiters);
295
315
  }
@@ -297,6 +317,81 @@ export function createRecycleGate({
297
317
  };
298
318
  }
299
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
+
300
395
  // --- Config ------------------------------------------------------------------
301
396
 
302
397
  function parseArgs() {
@@ -3608,11 +3703,16 @@ async function runPrerender(config) {
3608
3703
  console.error(' npm i -D puppeteer-core @sparticuz/chromium');
3609
3704
  process.exit(1);
3610
3705
  }
3706
+ // Headless shell: new headless can hang captureScreenshot on macOS
3611
3707
  return await puppeteer.default.launch({
3612
- headless: true,
3708
+ headless: 'shell',
3613
3709
  args: [
3614
3710
  '--no-sandbox',
3615
3711
  '--disable-setuid-sandbox',
3712
+ // Background tabs must keep producing frames and timers
3713
+ '--disable-background-timer-throttling',
3714
+ '--disable-backgrounding-occluded-windows',
3715
+ '--disable-renderer-backgrounding',
3616
3716
  ],
3617
3717
  });
3618
3718
  }
@@ -3673,9 +3773,10 @@ async function runPrerender(config) {
3673
3773
  every: browserRecycleEvery,
3674
3774
  drainTimeoutMs,
3675
3775
  recycleTimeoutMs,
3776
+ healthy: () => !!browser && browser.connected !== false,
3676
3777
  onLog: (message) => process.stderr.write(`prerender: ${message}\n`),
3677
- recycle: async (processed) => {
3678
- 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`);
3679
3780
  await closeBrowser(browser);
3680
3781
  browser = await launchBrowserBounded();
3681
3782
  },
@@ -3833,6 +3934,7 @@ async function runPrerender(config) {
3833
3934
  // No recycle can run while the caller holds a gate slot, so `browser` is
3834
3935
  // live for the whole call.
3835
3936
  const page = await browser.newPage();
3937
+ if (attempt) attempt.cancel = () => page.close();
3836
3938
  // Render at a typical desktop viewport so layouts dependent on viewport
3837
3939
  // width (responsive flex/grid, container queries, media queries) settle
3838
3940
  // into their desktop variant. Without this the headless default (often
@@ -4072,7 +4174,7 @@ async function runPrerender(config) {
4072
4174
  // small ones settle in well under 1 second.
4073
4175
  await page.waitForNetworkIdle({ idleTime: 1000, timeout: 8000 }).catch(() => { });
4074
4176
 
4075
- // DOM stability after network idle.
4177
+ // DOM stability after network idle (hard-capped).
4076
4178
  await page.evaluate(() => {
4077
4179
  return new Promise((resolve) => {
4078
4180
  const observer = new MutationObserver(() => {
@@ -4080,8 +4182,9 @@ async function runPrerender(config) {
4080
4182
  stable = setTimeout(finish, 500);
4081
4183
  });
4082
4184
  observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true });
4083
- const finish = () => { observer.disconnect(); resolve(); };
4185
+ const finish = () => { clearTimeout(cap); observer.disconnect(); resolve(); };
4084
4186
  let stable = setTimeout(finish, 500);
4187
+ const cap = setTimeout(finish, 15000);
4085
4188
  });
4086
4189
  }).catch(() => { });
4087
4190
 
@@ -4112,7 +4215,11 @@ async function runPrerender(config) {
4112
4215
  const ogImageHandled = !!config.seo.meta?.image
4113
4216
  || await page.evaluate(() => !!document.head.querySelector('meta[property="og:image"]'));
4114
4217
  if (!ogImageHandled) {
4115
- earlySnapshotUrl = await takeOgSnapshot(page, config.output, is404 ? '__404__' : pathSeg, globalAssetSig, ogCacheDir);
4218
+ // Bounded: a hung CDP call costs only this page's og image
4219
+ earlySnapshotUrl = await Promise.race([
4220
+ takeOgSnapshot(page, config.output, is404 ? '__404__' : pathSeg, globalAssetSig, ogCacheDir),
4221
+ new Promise((resolve) => setTimeout(() => resolve(null), 30000)),
4222
+ ]);
4116
4223
  }
4117
4224
  }
4118
4225
 
@@ -4286,6 +4393,25 @@ async function runPrerender(config) {
4286
4393
  await page.evaluate(() => {
4287
4394
  document.querySelectorAll('[x-markdown]').forEach((el) => {
4288
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
+ }
4289
4415
  el.removeAttribute('x-markdown');
4290
4416
  // Clean up opacity-0 + transition inline styles the plugin left behind.
4291
4417
  const style = el.getAttribute('style') || '';
@@ -4715,14 +4841,17 @@ async function runPrerender(config) {
4715
4841
  if (name.startsWith(':')) boundAttr = name.slice(1);
4716
4842
  else if (name.startsWith('x-bind:')) boundAttr = name.slice('x-bind:'.length);
4717
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.
4718
4852
  const concrete = node.getAttribute(boundAttr);
4853
+ node.removeAttribute(name);
4719
4854
  if (concrete != null && String(concrete).trim() !== '') {
4720
- // Removing :foo triggers Alpine's binding teardown, which
4721
- // restores the bound attribute to its pre-binding value
4722
- // (empty for clones whose template had no static class).
4723
- // Snapshot the eval'd value and re-set it after removal so
4724
- // the baked attribute survives the unbind.
4725
- node.removeAttribute(name);
4726
4855
  node.setAttribute(boundAttr, concrete);
4727
4856
  }
4728
4857
  continue;
@@ -5161,103 +5290,32 @@ async function runPrerender(config) {
5161
5290
  }
5162
5291
  }
5163
5292
 
5164
- // Phase 1: Puppeteer — render base paths, cache raw DOM for substitution.
5165
- // Any failures (e.g. transient navigation timeouts) are retried up to
5166
- // `maxRetries` times with a short backoff before being reported as fatal.
5167
- //
5168
- // Browser recycling: after every `browserRecycleEvery` successful pages the
5169
- // gate pauses new work, drains the in-flight pages, swaps the browser and
5170
- // resumes. Workers hold a gate slot only while they touch the browser.
5171
- //
5172
- // A failure whose message matches a transient Puppeteer/browser-recycle race
5173
- // (detached frame, destroyed execution context) is requeued immediately —
5174
- // no backoff — since waiting doesn't help a race and each attempt already
5175
- // gets its own fresh `pageTimeoutMs` budget.
5293
+ // Phase 1: Puppeteer — render base paths, cache raw DOM for substitution
5176
5294
  async function renderPuppeteerPaths(paths) {
5177
- let index = 0;
5178
- const total = paths.length;
5179
-
5180
- async function worker() {
5181
- while (true) {
5182
- const i = index++;
5183
- if (i >= paths.length) return;
5184
- const pathSeg = paths[i];
5185
- const displayPath = pathSeg === '' ? '/' : (pathSeg === NOT_FOUND_PATH ? '/__prerender_404__' : '/' + pathSeg);
5186
- let attempt = 0;
5187
- while (true) {
5188
- // Per-attempt result: `failedPaths` is shared, so a retry must never
5189
- // read or pop entries another worker pushed.
5190
- const attemptToken = { abandoned: false, failure: null };
5191
- await recycleGate.acquire();
5192
- try {
5193
- // Watchdog: a wedged renderer never returns, so cap the attempt.
5194
- const finished = await withDeadline(
5195
- processPath(pathSeg, i, {
5196
- attempt: attemptToken,
5197
- onRawHtml: (seg, html) => {
5198
- if (seg !== NOT_FOUND_PATH) baseHtmlCache.set(seg || '', html);
5199
- },
5200
- onLocaleDependentContent: promoteLocaleVariants,
5201
- }),
5202
- pageTimeoutMs
5203
- );
5204
- if (!finished) {
5205
- attemptToken.abandoned = true;
5206
- attemptToken.failure = { path: displayPath, message: `page render exceeded ${pageTimeoutMs}ms` };
5207
- // A timed-out page is presumed to have wedged the browser (e.g.
5208
- // a hung page.evaluate on a dead Chrome process) — force a
5209
- // recycle once this path is done, even if a later retry
5210
- // "succeeds" by reusing the same possibly-poisoned browser.
5211
- recycleGate.requestRecycle();
5212
- }
5213
- } catch (err) {
5214
- // Unexpected exception escaped processPath (e.g. browser died mid-call).
5215
- attemptToken.failure = {
5216
- path: displayPath,
5217
- message: err && err.message ? err.message : String(err),
5218
- };
5219
- } finally {
5220
- recycleGate.release();
5221
- }
5222
- if (!attemptToken.failure) {
5223
- recycleGate.countPage();
5224
- break; // success
5225
- }
5226
- if (attempt >= maxRetries) {
5227
- // Exhausted retries — likely an unstable browser (e.g. cascading
5228
- // "detached Frame" errors). Force the next path onto a fresh one.
5229
- failedPaths.push(attemptToken.failure);
5230
- if (failedPaths.length <= 10) {
5231
- process.stderr.write(`prerender: failed ${displayPath}: ${attemptToken.failure.message}\n`);
5232
- }
5233
- recycleGate.countPage();
5234
- recycleGate.requestRecycle();
5235
- break;
5236
- }
5237
- const immediateRequeue = isTransientFrameError(attemptToken.failure.message);
5238
- // Halfway through retries with no success → preemptively recycle the
5239
- // browser before the next attempt. This unblocks cascading frame
5240
- // failures where the browser process needs a fresh start. Skipped
5241
- // for a transient frame error — that's likely already a recycle
5242
- // race, not a signal to force another one.
5243
- if (!immediateRequeue && attempt + 1 >= Math.ceil(maxRetries / 2) && recycleGate.pages > 0) {
5244
- recycleGate.requestRecycle();
5245
- await recycleGate.maybeRecycle();
5246
- }
5247
- attempt++;
5248
- process.stderr.write(
5249
- `prerender: retrying ${displayPath} (${attemptToken.failure.message}) (attempt ${attempt + 1}/${maxRetries + 1})${immediateRequeue ? ' — immediate requeue' : ''}\n`
5250
- );
5251
- if (!immediateRequeue) await new Promise((r) => setTimeout(r, 500 * attempt));
5252
- }
5253
- // Attempt recycle after each completed path (only one worker performs
5254
- // it; the rest block in acquire()).
5255
- await recycleGate.maybeRecycle();
5256
- }
5257
- }
5258
- await Promise.all(
5259
- Array.from({ length: Math.min(concurrency, total || 1) }, () => worker())
5260
- );
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);
5261
5319
  }
5262
5320
 
5263
5321
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-render",
3
- "version": "0.5.43",
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/andrewmatlock/Manifest.git",
35
+ "url": "git+https://github.com/Manifest-X/Manifest.git",
36
36
  "directory": "packages/render"
37
37
  }
38
38
  }