mnfst-render 0.5.52 → 0.5.53
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 +238 -12
- package/package.json +1 -1
package/manifest.render.mjs
CHANGED
|
@@ -411,6 +411,8 @@ function parseArgs() {
|
|
|
411
411
|
if (args[i] === '--retries' && args[i + 1]) { out.retries = parseInt(args[++i], 10); continue; }
|
|
412
412
|
if (args[i] === '--incremental') { out.incremental = true; continue; }
|
|
413
413
|
if (args[i] === '--no-incremental') { out.noIncremental = true; continue; }
|
|
414
|
+
if (args[i] === '--run-cache') { out.runCache = true; continue; }
|
|
415
|
+
if (args[i] === '--no-run-cache') { out.runCache = false; continue; }
|
|
414
416
|
if (args[i] === '--dry-run') { out.dryRun = true; continue; }
|
|
415
417
|
if (args[i] === '--force-lock') { out.forceLock = true; continue; }
|
|
416
418
|
if (args[i] === '--debug-prerender') { out.debugPrerender = true; continue; }
|
|
@@ -614,6 +616,8 @@ function resolveConfig() {
|
|
|
614
616
|
// pattern). blockTelemetry (default on) adds TELEMETRY_BLOCK_PATTERNS.
|
|
615
617
|
blockRequests: Array.isArray(pre.blockRequests) ? pre.blockRequests.map((p) => String(p)).filter(Boolean) : [],
|
|
616
618
|
blockTelemetry: pre.blockTelemetry !== false,
|
|
619
|
+
// Run-scoped cross-origin response cache (render.runCache / --no-run-cache).
|
|
620
|
+
runCache: cli.runCache ?? pre.runCache !== false,
|
|
617
621
|
dryRun: !!cli.dryRun,
|
|
618
622
|
forceLock: !!cli.forceLock,
|
|
619
623
|
debugPrerender: !!cli.debugPrerender,
|
|
@@ -3524,7 +3528,9 @@ const PINNED_VERSION_RE = /@\d+\.\d+\.\d+/;
|
|
|
3524
3528
|
const EXTERNAL_BODY_HASH_LIMIT = 8 * 1024 * 1024;
|
|
3525
3529
|
|
|
3526
3530
|
// A prerender is not a visitor (Playcom measured 465 fake GA page_views per
|
|
3527
|
-
// render)
|
|
3531
|
+
// render), and error/RUM reporters phone home from headless too (a widget
|
|
3532
|
+
// crash in headless spams the site's production error tracker every render).
|
|
3533
|
+
// Analytics + error/RUM collection endpoints only — never script loads.
|
|
3528
3534
|
const TELEMETRY_BLOCK_PATTERNS = [
|
|
3529
3535
|
'google-analytics.com/g/collect',
|
|
3530
3536
|
'*.google-analytics.com/mp/collect',
|
|
@@ -3535,6 +3541,12 @@ const TELEMETRY_BLOCK_PATTERNS = [
|
|
|
3535
3541
|
'api.mixpanel.com/track',
|
|
3536
3542
|
'.facebook.com/tr?',
|
|
3537
3543
|
'.facebook.com/tr/',
|
|
3544
|
+
'ingest.sentry.io',
|
|
3545
|
+
'notify.bugsnag.com',
|
|
3546
|
+
'api.rollbar.com',
|
|
3547
|
+
'cloudflareinsights.com/cdn-cgi/rum',
|
|
3548
|
+
'bam.nr-data.net',
|
|
3549
|
+
'browser-intake-datadoghq.com',
|
|
3538
3550
|
];
|
|
3539
3551
|
|
|
3540
3552
|
/** URL predicate from block patterns: substring match, `*` globs within a
|
|
@@ -3549,6 +3561,106 @@ function compileBlockMatcher(patterns) {
|
|
|
3549
3561
|
return (url) => regs.some((r) => r.test(String(url)));
|
|
3550
3562
|
}
|
|
3551
3563
|
|
|
3564
|
+
// --- Run-scoped cross-origin response cache ------------------------------------
|
|
3565
|
+
// Within one run every cross-origin GET is fetched AT MOST ONCE: the first 2xx
|
|
3566
|
+
// body+headers are replayed to every later page. A prerender is a point-in-
|
|
3567
|
+
// time snapshot, so run-level consistency is a feature — and N pages
|
|
3568
|
+
// re-fetching identical framework assets manufactures the burst that fails.
|
|
3569
|
+
const RUN_CACHE_MAX_ENTRY_BYTES = EXTERNAL_BODY_HASH_LIMIT;
|
|
3570
|
+
const RUN_CACHE_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
3571
|
+
const RUN_CACHE_MAX_ENTRIES = 500;
|
|
3572
|
+
|
|
3573
|
+
/** Interception verdict: blocked URLs abort FIRST (never cached/served); only
|
|
3574
|
+
* cross-origin http(s) GETs are cache-eligible; everything else passes. */
|
|
3575
|
+
function classifyRunCacheRequest({ url, method, localOrigin, isBlocked }) {
|
|
3576
|
+
if (isBlocked && isBlocked(url)) return 'block';
|
|
3577
|
+
let u;
|
|
3578
|
+
try { u = new URL(url); } catch { return 'bypass'; }
|
|
3579
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:') return 'bypass';
|
|
3580
|
+
if (method !== 'GET' || u.origin === localOrigin) return 'bypass';
|
|
3581
|
+
return 'cache';
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3584
|
+
/** Headers safe to replay via request.respond: the stored body is the decoded
|
|
3585
|
+
* bytes, so the original transfer's encoding/length no longer apply. ETag,
|
|
3586
|
+
* Last-Modified and Content-Type pass through verbatim — capture must observe
|
|
3587
|
+
* identical metadata on served and fetched responses. */
|
|
3588
|
+
function runCacheReplayHeaders(headers) {
|
|
3589
|
+
const out = {};
|
|
3590
|
+
for (const [k, v] of Object.entries(headers || {})) {
|
|
3591
|
+
const key = k.toLowerCase();
|
|
3592
|
+
if (key === 'content-encoding' || key === 'content-length'
|
|
3593
|
+
|| key === 'transfer-encoding' || key === 'set-cookie') continue;
|
|
3594
|
+
out[k] = v;
|
|
3595
|
+
}
|
|
3596
|
+
return out;
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
function createRunCache({
|
|
3600
|
+
maxEntryBytes = RUN_CACHE_MAX_ENTRY_BYTES,
|
|
3601
|
+
maxTotalBytes = RUN_CACHE_MAX_TOTAL_BYTES,
|
|
3602
|
+
maxEntries = RUN_CACHE_MAX_ENTRIES,
|
|
3603
|
+
} = {}) {
|
|
3604
|
+
const entries = new Map(); // url → { status, headers, body }; insertion = recency
|
|
3605
|
+
const inflight = new Map(); // url → { promise, settle }
|
|
3606
|
+
let totalBytes = 0;
|
|
3607
|
+
const stats = { served: 0, servedUrls: new Set(), stored: 0 };
|
|
3608
|
+
return {
|
|
3609
|
+
stats,
|
|
3610
|
+
get size() { return entries.size; },
|
|
3611
|
+
get totalBytes() { return totalBytes; },
|
|
3612
|
+
/** Hit refreshes LRU recency. */
|
|
3613
|
+
get(url) {
|
|
3614
|
+
const e = entries.get(url);
|
|
3615
|
+
if (!e) return null;
|
|
3616
|
+
entries.delete(url); entries.set(url, e);
|
|
3617
|
+
return e;
|
|
3618
|
+
},
|
|
3619
|
+
peek: (url) => entries.get(url) || null,
|
|
3620
|
+
/** First 2xx wins; failures and over-cap bodies are never cached. */
|
|
3621
|
+
set(url, entry) {
|
|
3622
|
+
const { status, body } = entry || {};
|
|
3623
|
+
if (!(status >= 200 && status < 300)) return false;
|
|
3624
|
+
if (!body || typeof body.length !== 'number' || body.length > maxEntryBytes) return false;
|
|
3625
|
+
if (entries.has(url)) return true;
|
|
3626
|
+
entries.set(url, entry);
|
|
3627
|
+
totalBytes += body.length;
|
|
3628
|
+
stats.stored++;
|
|
3629
|
+
while (entries.size > maxEntries || totalBytes > maxTotalBytes) {
|
|
3630
|
+
const oldest = entries.keys().next().value;
|
|
3631
|
+
totalBytes -= entries.get(oldest).body.length;
|
|
3632
|
+
entries.delete(oldest);
|
|
3633
|
+
}
|
|
3634
|
+
return true;
|
|
3635
|
+
},
|
|
3636
|
+
noteServed(url) { stats.served++; stats.servedUrls.add(url); },
|
|
3637
|
+
flight: (url) => inflight.get(url) || null,
|
|
3638
|
+
/** One network fetch per URL at a time: concurrent pages await the first.
|
|
3639
|
+
* The timer settles null if nothing ever answers (page died mid-flight). */
|
|
3640
|
+
beginFlight(url, timeoutMs = 30000) {
|
|
3641
|
+
let resolve;
|
|
3642
|
+
const promise = new Promise((r) => { resolve = r; });
|
|
3643
|
+
const f = {
|
|
3644
|
+
promise,
|
|
3645
|
+
settle(entry) {
|
|
3646
|
+
if (inflight.get(url) === f) inflight.delete(url);
|
|
3647
|
+
clearTimeout(timer);
|
|
3648
|
+
resolve(entry || null);
|
|
3649
|
+
},
|
|
3650
|
+
};
|
|
3651
|
+
const timer = setTimeout(() => f.settle(null), timeoutMs);
|
|
3652
|
+
timer.unref?.();
|
|
3653
|
+
inflight.set(url, f);
|
|
3654
|
+
return f;
|
|
3655
|
+
},
|
|
3656
|
+
settleFlight(url, entry) { inflight.get(url)?.settle(entry); },
|
|
3657
|
+
};
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3660
|
+
/** Navigation budget: min(pageTimeout, 60s), keeping pageTimeout's 0=disabled. */
|
|
3661
|
+
const deriveNavTimeoutMs = (pageTimeoutMs) =>
|
|
3662
|
+
(pageTimeoutMs > 0 ? Math.min(pageTimeoutMs, 60000) : 0);
|
|
3663
|
+
|
|
3552
3664
|
/** Cross-origin response → { validator } | { flag } | {} (2xx without
|
|
3553
3665
|
* validator: content-address the body). A 304 is the page's OWN conditional
|
|
3554
3666
|
* revalidation answered "unchanged" — a hit, not a failure: the validator
|
|
@@ -3577,9 +3689,7 @@ function classifyExternalResponse(status, responseHeaders = {}, requestHeaders =
|
|
|
3577
3689
|
* not a failure, never awaited.
|
|
3578
3690
|
*/
|
|
3579
3691
|
function createCaptureHooks({ capture, localOrigin, resolveRequest, isBlocked }) {
|
|
3580
|
-
const flag = (reason) =>
|
|
3581
|
-
if (!capture.nonCacheable) { capture.nonCacheable = true; capture.reason = reason; }
|
|
3582
|
-
};
|
|
3692
|
+
const flag = (reason) => flagNonCacheable(capture, reason);
|
|
3583
3693
|
return {
|
|
3584
3694
|
request({ url, method }) {
|
|
3585
3695
|
try {
|
|
@@ -3638,6 +3748,28 @@ function createCaptureHooks({ capture, localOrigin, resolveRequest, isBlocked })
|
|
|
3638
3748
|
};
|
|
3639
3749
|
}
|
|
3640
3750
|
|
|
3751
|
+
// ALL distinct non-cacheable causes, not just the first: one render surfaces
|
|
3752
|
+
// every third party to block/fix instead of one per 65-minute run.
|
|
3753
|
+
const NONCACHEABLE_REASONS_CAP = 20;
|
|
3754
|
+
|
|
3755
|
+
/** URLs in a cause reduced to host+path (no scheme/query) so per-page query
|
|
3756
|
+
* strings collapse into one actionable line. */
|
|
3757
|
+
function normalizeNonCacheableCause(reason) {
|
|
3758
|
+
return String(reason).replace(/https?:\/\/[^\s]+/g, (m) => {
|
|
3759
|
+
try { const u = new URL(m); return u.host + u.pathname; } catch { return m; }
|
|
3760
|
+
});
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3763
|
+
/** `.reason` stays the first cause (ledger back-compat); `.reasons` collects
|
|
3764
|
+
* every distinct cause, ordered, deduped on the normalized form, capped. */
|
|
3765
|
+
function flagNonCacheable(capture, reason) {
|
|
3766
|
+
if (!capture.nonCacheable) { capture.nonCacheable = true; capture.reason = reason; }
|
|
3767
|
+
const list = capture.reasons || (capture.reasons = []);
|
|
3768
|
+
if (list.length >= NONCACHEABLE_REASONS_CAP) return;
|
|
3769
|
+
const key = normalizeNonCacheableCause(reason);
|
|
3770
|
+
if (!list.some((r) => normalizeNonCacheableCause(r) === key)) list.push(reason);
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3641
3773
|
const externalValidatorToken = (url, v) =>
|
|
3642
3774
|
`${url}#etag:${v?.etag || ''}|lm:${v?.lastModified || ''}|ch:${v?.contentHash || ''}`;
|
|
3643
3775
|
|
|
@@ -3646,8 +3778,7 @@ const externalValidatorToken = (url, v) =>
|
|
|
3646
3778
|
* UNVERIFIED external dependency — the route must render. */
|
|
3647
3779
|
function settleCaptureAwaiting(capture) {
|
|
3648
3780
|
if (capture.awaiting && capture.awaiting.size > 0 && !capture.nonCacheable) {
|
|
3649
|
-
capture
|
|
3650
|
-
capture.reason = `unverified external: ${[...capture.awaiting][0]}`;
|
|
3781
|
+
flagNonCacheable(capture, `unverified external: ${[...capture.awaiting][0]}`);
|
|
3651
3782
|
}
|
|
3652
3783
|
return capture;
|
|
3653
3784
|
}
|
|
@@ -3915,7 +4046,13 @@ function createIncrementalRun({ rootResolved, previousOutput, stagingOutput, glo
|
|
|
3915
4046
|
const routes = {};
|
|
3916
4047
|
for (const [key, entry] of carried) routes[key] = entry;
|
|
3917
4048
|
for (const [key, cap] of captures) {
|
|
3918
|
-
if (cap.nonCacheable) {
|
|
4049
|
+
if (cap.nonCacheable) {
|
|
4050
|
+
routes[key] = { nonCacheable: true, reason: cap.reason };
|
|
4051
|
+
// Additive: tolerated by the malformed-entry check (non-cacheable
|
|
4052
|
+
// entries short-circuit before the shape test), so no schema bump.
|
|
4053
|
+
if (Array.isArray(cap.reasons) && cap.reasons.length) routes[key].reasons = cap.reasons;
|
|
4054
|
+
continue;
|
|
4055
|
+
}
|
|
3919
4056
|
const requested = {};
|
|
3920
4057
|
const requestedPairs = [];
|
|
3921
4058
|
for (const pathname of [...cap.requested.keys()].sort()) {
|
|
@@ -4422,6 +4559,9 @@ async function runPrerender(config) {
|
|
|
4422
4559
|
// a wedged page.evaluate() may have poisoned the shared browser process.
|
|
4423
4560
|
// pageTimeout is configurable via manifest.prerender.pageTimeout; 0 disables.
|
|
4424
4561
|
const pageTimeoutMs = Math.max(0, pre.pageTimeout ?? Math.max(120000, (config.wait ?? 30000) * 4));
|
|
4562
|
+
// Navigation shares the page budget up to 60s (was a hard 30s cap that
|
|
4563
|
+
// failed runs while a 120s page budget sat unused); 0 stays disabled.
|
|
4564
|
+
const navTimeoutMs = deriveNavTimeoutMs(pageTimeoutMs);
|
|
4425
4565
|
// The recycle gate must never swap the browser out from under a live page,
|
|
4426
4566
|
// so its drain wait has to outlast the per-page timeout above (every page
|
|
4427
4567
|
// is guaranteed to release its slot by then). Configurable via
|
|
@@ -4655,6 +4795,12 @@ async function runPrerender(config) {
|
|
|
4655
4795
|
const cdpBlockedUrls = blockPatterns.map((p) => `*${p}*`);
|
|
4656
4796
|
let telemetryBlockedCount = 0;
|
|
4657
4797
|
|
|
4798
|
+
// Run cache (render.runCache, default on): one cross-origin fetch per URL per
|
|
4799
|
+
// run, replayed to every later page. With it active, blocking folds into the
|
|
4800
|
+
// same interception path (Network.setBlockedURLs does not layer under
|
|
4801
|
+
// request interception).
|
|
4802
|
+
const runCache = config.runCache ? createRunCache() : null;
|
|
4803
|
+
|
|
4658
4804
|
const puppeteerRenderPaths = inc ? await inc.partition(puppeteerPaths) : puppeteerPaths;
|
|
4659
4805
|
let incRendered = puppeteerRenderPaths.length;
|
|
4660
4806
|
const puppeteerTotal = puppeteerRenderPaths.length;
|
|
@@ -4707,7 +4853,7 @@ async function runPrerender(config) {
|
|
|
4707
4853
|
capture = {
|
|
4708
4854
|
requested: new Map(), files: new Set(),
|
|
4709
4855
|
externalPinned: new Set(), externalValidated: new Map(),
|
|
4710
|
-
awaiting: new Set(), pending: [], nonCacheable: false, reason: null,
|
|
4856
|
+
awaiting: new Set(), pending: [], nonCacheable: false, reason: null, reasons: [],
|
|
4711
4857
|
};
|
|
4712
4858
|
const hooks = createCaptureHooks({
|
|
4713
4859
|
capture,
|
|
@@ -4734,9 +4880,60 @@ async function runPrerender(config) {
|
|
|
4734
4880
|
// viewport-driven flex distribution.
|
|
4735
4881
|
await page.setViewport({ width: 1200, height: 800, deviceScaleFactor: 1 });
|
|
4736
4882
|
try {
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4883
|
+
if (runCache) {
|
|
4884
|
+
// Run cache via request interception. Blocked URLs abort FIRST and are
|
|
4885
|
+
// never cached or served; same-origin and non-GET traffic passes
|
|
4886
|
+
// through untouched. A served response is a real fulfilled response:
|
|
4887
|
+
// the page.on capture listeners above observe it like a network one.
|
|
4888
|
+
const pageLocalOrigin = new URL(config.localUrl).origin;
|
|
4889
|
+
const done = (p) => p.catch(() => { /* page/request gone */ });
|
|
4890
|
+
await page.setRequestInterception(true);
|
|
4891
|
+
page.on('request', (req) => {
|
|
4892
|
+
try {
|
|
4893
|
+
const verdict = classifyRunCacheRequest({
|
|
4894
|
+
url: req.url(), method: req.method(),
|
|
4895
|
+
localOrigin: pageLocalOrigin, isBlocked: blockMatcher,
|
|
4896
|
+
});
|
|
4897
|
+
if (verdict === 'block') return done(req.abort('blockedbyclient'));
|
|
4898
|
+
if (verdict === 'bypass') return done(req.continue());
|
|
4899
|
+
const href = new URL(req.url()).href;
|
|
4900
|
+
const hit = runCache.get(href);
|
|
4901
|
+
if (hit) { runCache.noteServed(href); return done(req.respond(hit)); }
|
|
4902
|
+
const flight = runCache.flight(href);
|
|
4903
|
+
if (flight) {
|
|
4904
|
+
return flight.promise.then((entry) => {
|
|
4905
|
+
if (entry) { runCache.noteServed(href); return done(req.respond(entry)); }
|
|
4906
|
+
return done(req.continue()); // first fetch failed: retry the network
|
|
4907
|
+
});
|
|
4908
|
+
}
|
|
4909
|
+
runCache.beginFlight(href);
|
|
4910
|
+
return done(req.continue());
|
|
4911
|
+
} catch { return done(req.continue()); }
|
|
4912
|
+
});
|
|
4913
|
+
page.on('response', (res) => {
|
|
4914
|
+
try {
|
|
4915
|
+
if (res.request().method() !== 'GET') return;
|
|
4916
|
+
const u = new URL(res.url());
|
|
4917
|
+
if ((u.protocol !== 'http:' && u.protocol !== 'https:') || u.origin === pageLocalOrigin) return;
|
|
4918
|
+
const prior = runCache.peek(u.href);
|
|
4919
|
+
if (prior) return runCache.settleFlight(u.href, prior);
|
|
4920
|
+
const status = res.status();
|
|
4921
|
+
if (status < 200 || status >= 300) return runCache.settleFlight(u.href, null);
|
|
4922
|
+
const headers = runCacheReplayHeaders(res.headers());
|
|
4923
|
+
res.buffer()
|
|
4924
|
+
.then((body) => {
|
|
4925
|
+
const entry = { status, headers, body };
|
|
4926
|
+
runCache.settleFlight(u.href, runCache.set(u.href, entry) ? entry : null);
|
|
4927
|
+
})
|
|
4928
|
+
.catch(() => runCache.settleFlight(u.href, null));
|
|
4929
|
+
} catch { /* not a cacheable response */ }
|
|
4930
|
+
});
|
|
4931
|
+
page.on('requestfailed', (req) => {
|
|
4932
|
+
try { runCache.settleFlight(new URL(req.url()).href, null); } catch { /* unparseable */ }
|
|
4933
|
+
});
|
|
4934
|
+
} else if (cdpBlockedUrls.length > 0) {
|
|
4935
|
+
// CDP URL blocking (telemetry + render.blockRequests): requests abort at
|
|
4936
|
+
// the network layer; coexists with the page.on capture listeners above.
|
|
4740
4937
|
const cdp = await page.createCDPSession();
|
|
4741
4938
|
await cdp.send('Network.enable');
|
|
4742
4939
|
await cdp.send('Network.setBlockedURLs', { urls: cdpBlockedUrls });
|
|
@@ -4918,7 +5115,7 @@ async function runPrerender(config) {
|
|
|
4918
5115
|
pushDebug({ path: displayPath, stage: 'start' });
|
|
4919
5116
|
await page.goto(url, {
|
|
4920
5117
|
waitUntil: 'domcontentloaded',
|
|
4921
|
-
timeout:
|
|
5118
|
+
timeout: navTimeoutMs,
|
|
4922
5119
|
});
|
|
4923
5120
|
|
|
4924
5121
|
// Settle waits. These give Manifest plugins (especially the components
|
|
@@ -6084,6 +6281,7 @@ async function runPrerender(config) {
|
|
|
6084
6281
|
externalValidated: capture.externalValidated,
|
|
6085
6282
|
nonCacheable: capture.nonCacheable,
|
|
6086
6283
|
reason: capture.reason,
|
|
6284
|
+
reasons: capture.reasons,
|
|
6087
6285
|
outputs,
|
|
6088
6286
|
utility: routeUtilityBlocks,
|
|
6089
6287
|
});
|
|
@@ -6202,6 +6400,27 @@ async function runPrerender(config) {
|
|
|
6202
6400
|
const why = Object.entries(inc.state.reasons).sort((a, b) => b[1] - a[1]).map(([r, c]) => `${c} ${r}`);
|
|
6203
6401
|
process.stdout.write(`prerender: skipped ${inc.state.skipped} unchanged, rendered ${incRendered}${why.length ? ` (${why.join(', ')})` : ''}\n`);
|
|
6204
6402
|
}
|
|
6403
|
+
// Every distinct non-cacheable cause across the run, host+path visible:
|
|
6404
|
+
// what to block or fix, found in ONE render instead of serially.
|
|
6405
|
+
const causes = new Map();
|
|
6406
|
+
for (const cap of inc.captures.values()) {
|
|
6407
|
+
if (!cap.nonCacheable) continue;
|
|
6408
|
+
const list = Array.isArray(cap.reasons) && cap.reasons.length ? cap.reasons : [cap.reason];
|
|
6409
|
+
for (const r of list) {
|
|
6410
|
+
if (!r) continue;
|
|
6411
|
+
const key = normalizeNonCacheableCause(r);
|
|
6412
|
+
causes.set(key, (causes.get(key) || 0) + 1);
|
|
6413
|
+
}
|
|
6414
|
+
}
|
|
6415
|
+
if (causes.size > 0) {
|
|
6416
|
+
const rows = [...causes.entries()].sort((a, b) => b[1] - a[1]);
|
|
6417
|
+
process.stdout.write('prerender: non-cacheable causes —\n');
|
|
6418
|
+
for (const [cause, n] of rows.slice(0, 10)) process.stdout.write(` ${n}× ${cause}\n`);
|
|
6419
|
+
if (rows.length > 10) process.stdout.write(` … +${rows.length - 10} more cause(s)\n`);
|
|
6420
|
+
}
|
|
6421
|
+
}
|
|
6422
|
+
if (runCache && runCache.stats.served > 0) {
|
|
6423
|
+
process.stdout.write(`prerender: run cache served ${runCache.stats.served} cross-origin requests (${runCache.stats.servedUrls.size} unique)\n`);
|
|
6205
6424
|
}
|
|
6206
6425
|
if (telemetryBlockedCount > 0) {
|
|
6207
6426
|
process.stdout.write(`prerender: blocked ${telemetryBlockedCount} telemetry requests (render.blockTelemetry: false to allow)\n`);
|
|
@@ -6336,4 +6555,11 @@ export {
|
|
|
6336
6555
|
createCaptureHooks,
|
|
6337
6556
|
compileBlockMatcher,
|
|
6338
6557
|
TELEMETRY_BLOCK_PATTERNS,
|
|
6558
|
+
createRunCache,
|
|
6559
|
+
classifyRunCacheRequest,
|
|
6560
|
+
runCacheReplayHeaders,
|
|
6561
|
+
deriveNavTimeoutMs,
|
|
6562
|
+
flagNonCacheable,
|
|
6563
|
+
normalizeNonCacheableCause,
|
|
6564
|
+
NONCACHEABLE_REASONS_CAP,
|
|
6339
6565
|
};
|