@thomas.f/prnext 0.1.16-alpha → 0.1.18-alpha
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/AI.md +63 -0
- package/README.md +35 -503
- package/build/ADAPTERS.md +27 -2
- package/build/actions.mjs +6 -1
- package/build/analyze.mjs +340 -0
- package/build/app-config.mjs +20 -8
- package/build/app-static.mjs +41 -14
- package/build/app-styles.mjs +79 -0
- package/build/app.mjs +99 -31
- package/build/build-lock.mjs +6 -6
- package/build/bundle-runtime.mjs +39 -11
- package/build/cache-components.mjs +1 -1
- package/build/cache-handlers.mjs +1 -1
- package/build/check.mjs +56 -11
- package/build/compact-css.mjs +53 -0
- package/build/compilation-cache.mjs +269 -0
- package/build/compilation-store.mjs +142 -0
- package/build/compiler.mjs +51 -4
- package/build/concurrent.mjs +12 -0
- package/build/config.mjs +50 -13
- package/build/create.mjs +239 -0
- package/build/css-modules.mjs +65 -10
- package/build/deployment-assets.mjs +89 -0
- package/build/deployment-config.mjs +18 -0
- package/build/dev-compiler-health.mjs +11 -0
- package/build/dev-css-update.mjs +85 -0
- package/build/dev-dependencies.mjs +44 -0
- package/build/dev-discovery.mjs +28 -0
- package/build/dev-worker.mjs +14 -5
- package/build/dev.mjs +64 -6
- package/build/dynamic.mjs +1 -1
- package/build/edge.mjs +5 -4
- package/build/fonts.mjs +1 -1
- package/build/framework-imports.mjs +50 -0
- package/build/html-only-config.mjs +53 -0
- package/build/html-only-graph.mjs +67 -0
- package/build/incremental.mjs +8 -4
- package/build/index.mjs +140 -112
- package/build/inspect.mjs +3 -0
- package/build/inspection-summary.mjs +37 -4
- package/build/instrumentation.mjs +30 -0
- package/build/loader-cache.mjs +53 -26
- package/build/metadata-routes.mjs +3 -4
- package/build/migrate.mjs +60 -57
- package/build/migration-audit.mjs +1 -1
- package/build/native-type-config.mjs +37 -0
- package/build/npm-packages.mjs +3 -18
- package/build/package-manager.mjs +308 -0
- package/build/persistent-cache.mjs +112 -0
- package/build/precompress.mjs +62 -30
- package/build/prerender-cache.mjs +70 -0
- package/build/prerender-config.mjs +29 -0
- package/build/prerender-pages.mjs +100 -0
- package/build/prerender-pool.mjs +165 -0
- package/build/prerender-process-worker.mjs +58 -0
- package/build/prerender-worker.mjs +10 -0
- package/build/production-builder.mjs +129 -0
- package/build/react-compiler-report.mjs +225 -0
- package/build/react-compiler.mjs +139 -0
- package/build/runtime-contract.mjs +2 -2
- package/build/scan.mjs +4 -4
- package/build/source-analysis.mjs +42 -0
- package/build/static-adapter.mjs +13 -5
- package/build/static-export.mjs +4 -4
- package/build/static-input.mjs +48 -0
- package/build/styles.mjs +17 -3
- package/build/transform-cache.mjs +37 -4
- package/build/transform.mjs +1 -1
- package/build/type-command.mjs +41 -0
- package/build/typed-routes.mjs +95 -0
- package/build/typescript-dev-worker.mjs +18 -0
- package/build/typescript-dev.mjs +49 -0
- package/build/typescript-native-worker.mjs +55 -0
- package/build/typescript-native.mjs +18 -0
- package/build/typescript-toolchain.mjs +19 -0
- package/build/typescript-watch.mjs +22 -0
- package/build/typescript.mjs +11 -8
- package/build/webpack-graph.mjs +170 -30
- package/build/worker-memory.mjs +9 -0
- package/build/worker-preload.mjs +9 -0
- package/cli.mjs +162 -122
- package/compat/activity-cache.cjs +26 -0
- package/compat/after.cjs +6 -4
- package/compat/app-layout-context.cjs +52 -2
- package/compat/base64.cjs +10 -0
- package/compat/cache-handlers.cjs +12 -2
- package/compat/cache-wire.cjs +28 -0
- package/compat/cache.cjs +2 -1
- package/compat/cache.d.cts +4 -0
- package/compat/cookies.cjs +21 -5
- package/compat/data-cache.cjs +12 -8
- package/compat/flight-delta.cjs +150 -0
- package/compat/headers.cjs +4 -2
- package/compat/incremental-cache.cjs +20 -7
- package/compat/index.d.ts +89 -10
- package/compat/link.cjs +12 -9
- package/compat/link.d.cts +4 -3
- package/compat/navigation-authorization.cjs +9 -0
- package/compat/navigation-stages.cjs +19 -0
- package/compat/navigation.d.cts +4 -3
- package/compat/offline-store.cjs +11 -0
- package/compat/offline.cjs +5 -0
- package/compat/offline.d.cts +2 -0
- package/compat/redis-cache.cjs +201 -0
- package/compat/redis-cache.d.cts +37 -0
- package/compat/router.d.cts +4 -3
- package/compat/server-primitives.cjs +31 -11
- package/compat/server.d.cts +15 -2
- package/compat/static-generation.cjs +7 -0
- package/compat/typed-route.d.cts +17 -0
- package/compat/use-cache.cjs +3 -0
- package/docs/ai/compatibility.md +275 -0
- package/docs/ai/data.md +288 -0
- package/docs/ai/deployment.md +269 -0
- package/docs/ai/manifest.json +11 -0
- package/docs/ai/quickstart.md +171 -0
- package/docs/ai/routing.md +214 -0
- package/docs/ai/troubleshooting.md +155 -0
- package/index.mjs +2 -0
- package/native/diagnostics.mjs +34 -0
- package/native/resolve.mjs +7 -2
- package/package.json +44 -7
- package/release/ai-docs.mjs +48 -0
- package/release/verify.mjs +2 -0
- package/runtime/action-client.mjs +10 -4
- package/runtime/action-server.mjs +2 -0
- package/runtime/ai-docs.mjs +202 -0
- package/runtime/app-client.mjs +226 -68
- package/runtime/app-content.mjs +4 -3
- package/runtime/app-partial-client.mjs +3 -0
- package/runtime/app-partial.mjs +1 -1
- package/runtime/app-prefetch-constants.mjs +4 -0
- package/runtime/app-prefetch.mjs +2 -3
- package/runtime/app-render-key.mjs +5 -0
- package/runtime/app-render.mjs +100 -28
- package/runtime/app-routing.mjs +22 -9
- package/runtime/app-static.mjs +18 -8
- package/runtime/app-stream-html.mjs +10 -53
- package/runtime/app-style-selection.mjs +34 -0
- package/runtime/app-styles.mjs +154 -0
- package/runtime/cache-components-rsc.mjs +4 -1
- package/runtime/cache-request.mjs +2 -1
- package/runtime/cloud/archive.mjs +5 -6
- package/runtime/compile-cache.mjs +8 -0
- package/runtime/deployment-client.mjs +19 -0
- package/runtime/dev-ai-brands.mjs +26 -0
- package/runtime/dev-ai-style.mjs +99 -0
- package/runtime/dev-build-reasons.mjs +25 -0
- package/runtime/dev-cache-control.mjs +53 -0
- package/runtime/dev-client.mjs +245 -46
- package/runtime/dev-compiler-broker.mjs +42 -0
- package/runtime/dev-console-copy.mjs +19 -0
- package/runtime/dev-console.mjs +106 -0
- package/runtime/dev-dependency-graph.mjs +40 -0
- package/runtime/dev-element-picker-style.mjs +27 -0
- package/runtime/dev-element-picker.mjs +213 -0
- package/runtime/dev-error-overlay-style.mjs +41 -0
- package/runtime/dev-error-overlay.mjs +167 -0
- package/runtime/dev-error-report.mjs +121 -0
- package/runtime/dev-file-changes.mjs +1 -1
- package/runtime/dev-gateway.mjs +44 -0
- package/runtime/dev-host-relay.mjs +66 -0
- package/runtime/dev-indicator.mjs +191 -0
- package/runtime/dev-issue-motion.mjs +214 -0
- package/runtime/dev-lazy-tool.mjs +34 -0
- package/runtime/dev-logo.mjs +2 -0
- package/runtime/dev-notification.mjs +81 -0
- package/runtime/dev-popout.mjs +34 -0
- package/runtime/dev-position.mjs +107 -0
- package/runtime/dev-preview-touch.mjs +122 -0
- package/runtime/dev-runtime-pool.mjs +45 -0
- package/runtime/dev-runtime-spare.mjs +57 -0
- package/runtime/dev-tools-client.mjs +155 -0
- package/runtime/dev-tools-mcp.mjs +126 -0
- package/runtime/dev-tools-metrics.mjs +31 -0
- package/runtime/dev-tools-model.mjs +42 -0
- package/runtime/dev-tools-panel.mjs +341 -0
- package/runtime/dev-tools-result.mjs +37 -0
- package/runtime/dev-tools-server.mjs +256 -0
- package/runtime/dev-tools-source.mjs +104 -0
- package/runtime/dev-tooltip.mjs +94 -0
- package/runtime/dev-ui-icons.mjs +8 -0
- package/runtime/dev-workbench-style.mjs +106 -0
- package/runtime/dev-workbench.mjs +428 -0
- package/runtime/dev-worker-logs.mjs +31 -0
- package/runtime/development.mjs +324 -0
- package/runtime/document.mjs +1 -1
- package/runtime/edge.mjs +6 -5
- package/runtime/env.mjs +2 -1
- package/runtime/fetch-cache.mjs +10 -8
- package/runtime/flight-delta.mjs +50 -0
- package/runtime/flight-tap.mjs +28 -0
- package/runtime/html-only.mjs +27 -0
- package/runtime/http.mjs +2 -0
- package/runtime/incremental-cache.mjs +25 -4
- package/runtime/instrumentation.mjs +49 -0
- package/runtime/navigation-recovery.mjs +36 -0
- package/runtime/navigation.mjs +7 -0
- package/runtime/node-html-stream.mjs +47 -0
- package/runtime/pages-client.mjs +24 -8
- package/runtime/pages-navigation.mjs +54 -64
- package/runtime/persistent/client.mjs +6 -5
- package/runtime/persistent/daemon.mjs +66 -56
- package/runtime/persistent/native-proxy.mjs +63 -0
- package/runtime/prefetch-scheduler.mjs +110 -0
- package/runtime/preload.mjs +12 -0
- package/runtime/profile.mjs +14 -0
- package/runtime/profiles.json +2 -2
- package/runtime/render.mjs +3 -0
- package/runtime/route-pattern.mjs +44 -0
- package/runtime/rsc-worker.mjs +41 -12
- package/runtime/trace.mjs +78 -0
- package/runtime/typed-route.mjs +19 -0
- package/runtime/watch-project.mjs +29 -18
- package/runtime/worker.mjs +102 -24
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
const MAX_VALUE = 2 * 1024 * 1024;
|
|
5
|
+
const MAX_BYTES = 32 * 1024 * 1024;
|
|
6
|
+
const MAX_ENTRIES = 256;
|
|
7
|
+
const LEASE_MS = 25_000;
|
|
8
|
+
const keyPattern = /^[a-f\d]{64}$/;
|
|
9
|
+
|
|
10
|
+
// A build-only, loopback capability. It shares only values which the existing
|
|
11
|
+
// runtime explicitly elects to cache. It never stores route output or fetches
|
|
12
|
+
// marked no-store. The entire store dies with this prerender session.
|
|
13
|
+
export async function createPrerenderCache() {
|
|
14
|
+
const token = randomBytes(32).toString('hex');
|
|
15
|
+
const values = new Map(), leases = new Map();
|
|
16
|
+
let bytes = 0, active = 0;
|
|
17
|
+
const generation = 0;
|
|
18
|
+
function remove(key) { const value = values.get(key); if (value) { bytes -= value.cost; values.delete(key); } }
|
|
19
|
+
function operation(input) {
|
|
20
|
+
if (input.op === 'generation') return { generation };
|
|
21
|
+
if (input.op === 'invalidate') throw new Error('Build cache invalidation requires a running production server');
|
|
22
|
+
if (!keyPattern.test(input.key)) throw new Error('Invalid build cache key');
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
for(const [key,lease] of leases)if(lease.until<=now)leases.delete(key);
|
|
25
|
+
const lease = leases.get(input.key);
|
|
26
|
+
if (input.op === 'leaseStatus') return { valid: Boolean(lease && lease.id === input.lease) };
|
|
27
|
+
if (input.op === 'release') { if (lease?.id === input.lease) leases.delete(input.key); return {}; }
|
|
28
|
+
if (input.op === 'commit') {
|
|
29
|
+
if (!lease || lease.id !== input.lease || typeof input.value !== 'string' || input.value.length > Math.ceil(MAX_VALUE / 3) * 4) return { stored: false };
|
|
30
|
+
const size = Buffer.byteLength(input.value, 'base64');
|
|
31
|
+
if (size > MAX_VALUE || (input.revalidate !== null && (!Number.isFinite(input.revalidate) || input.revalidate < 0))) return { stored: false };
|
|
32
|
+
remove(input.key);
|
|
33
|
+
const cost = Buffer.byteLength(input.value) + 512;
|
|
34
|
+
while (values.size && (bytes + cost > MAX_BYTES || values.size >= MAX_ENTRIES)) remove(values.keys().next().value);
|
|
35
|
+
values.set(input.key, { value: input.value, cost, timestamp: now, revalidate: input.revalidate, expire: input.expire }); bytes += cost;
|
|
36
|
+
leases.delete(input.key);
|
|
37
|
+
return { stored: true };
|
|
38
|
+
}
|
|
39
|
+
if (input.op !== 'read') throw new Error('Unknown build cache operation');
|
|
40
|
+
const record = values.get(input.key);
|
|
41
|
+
const ttl = Math.min(input.revalidate ?? Infinity, record?.revalidate ?? Infinity, record?.expire ?? Infinity);
|
|
42
|
+
if (record && now - record.timestamp < ttl * 1000) {
|
|
43
|
+
values.delete(input.key); values.set(input.key, record);
|
|
44
|
+
return { state: 'fresh', value: record.value, key: input.key, generation };
|
|
45
|
+
}
|
|
46
|
+
if (record) remove(input.key);
|
|
47
|
+
if (lease) return { state: 'pending', retryAfterMs: 20, key: input.key, generation };
|
|
48
|
+
if (leases.size >= MAX_ENTRIES) throw new Error('Build cache lease capacity exceeded');
|
|
49
|
+
const id = randomBytes(16).toString('hex'); leases.set(input.key, { id, until: now + LEASE_MS });
|
|
50
|
+
return { state: 'miss', lease: id, key: input.key, generation };
|
|
51
|
+
}
|
|
52
|
+
const server = createServer(async (request, response) => {
|
|
53
|
+
if(active>=8){response.writeHead(503).end();return;}
|
|
54
|
+
active++;
|
|
55
|
+
try {
|
|
56
|
+
const auth = Buffer.from(request.headers.authorization || '');
|
|
57
|
+
const expected = Buffer.from(`Bearer ${token}`);
|
|
58
|
+
if (request.method !== 'POST' || request.url !== '/' || auth.length !== expected.length || !timingSafeEqual(auth, expected)) { response.writeHead(403).end(); return; }
|
|
59
|
+
let size = 0; const chunks = [];
|
|
60
|
+
for await (const chunk of request) { size += chunk.length; if (size > 3 * 1024 * 1024) throw new Error('Build cache request too large'); chunks.push(chunk); }
|
|
61
|
+
const result = operation(JSON.parse(Buffer.concat(chunks).toString()));
|
|
62
|
+
response.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(JSON.stringify(result));
|
|
63
|
+
} catch { if (!response.headersSent) response.writeHead(400); response.end(); }
|
|
64
|
+
finally{active--;}
|
|
65
|
+
});
|
|
66
|
+
server.requestTimeout = 5000; server.headersTimeout = 5000;
|
|
67
|
+
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); });
|
|
68
|
+
const env = { PRNEXT_CACHE_URL: `http://127.0.0.1:${server.address().port}/`, PRNEXT_CACHE_TOKEN: token };
|
|
69
|
+
return { env, async close() { values.clear(); leases.clear(); bytes = 0; await new Promise(resolve => { server.close(resolve); server.closeAllConnections(); }); } };
|
|
70
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { availableParallelism, freemem } from 'node:os';
|
|
2
|
+
|
|
3
|
+
const MIB = 1024 * 1024;
|
|
4
|
+
|
|
5
|
+
export function validatePrerenderConfig(value) {
|
|
6
|
+
if (value === undefined) return {};
|
|
7
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError('prerender must be an object.');
|
|
8
|
+
for (const key of Object.keys(value)) if (!['workers', 'maxMemoryMb'].includes(key)) throw new TypeError(`prerender.${key} is not supported.`);
|
|
9
|
+
const result = {};
|
|
10
|
+
if (value.workers !== undefined) {
|
|
11
|
+
if (!Number.isInteger(value.workers) || value.workers < 1 || value.workers > 8) throw new TypeError('prerender.workers must be an integer from 1 to 8.');
|
|
12
|
+
result.workers = value.workers;
|
|
13
|
+
}
|
|
14
|
+
if (value.maxMemoryMb !== undefined) {
|
|
15
|
+
if (!Number.isInteger(value.maxMemoryMb) || value.maxMemoryMb < 256 || value.maxMemoryMb > 16384) throw new TypeError('prerender.maxMemoryMb must be an integer from 256 to 16384.');
|
|
16
|
+
result.maxMemoryMb = value.maxMemoryMb;
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// This is an admission budget, not an OS memory sandbox. Reserve memory for the
|
|
22
|
+
// coordinator/compiler and derive the process count from currently free RAM.
|
|
23
|
+
export function prerenderResources(value, { cpus = availableParallelism(), free = Math.min(freemem(), process.availableMemory?.() ?? Infinity) } = {}) {
|
|
24
|
+
const config = validatePrerenderConfig(value);
|
|
25
|
+
const availableMb = Math.max(0, Math.floor(free / MIB) - 512);
|
|
26
|
+
const maxMemoryMb = Math.min(config.maxMemoryMb ?? 2048, Math.max(256, Math.floor(availableMb / 2)));
|
|
27
|
+
const workers = Math.max(1, Math.min(config.workers ?? 4, Math.max(1, cpus - 1), Math.floor(maxMemoryMb / 384)));
|
|
28
|
+
return { workers, maxMemoryMb, heapMb: Math.max(64, Math.floor(maxMemoryMb / workers / 3)), explicit: config.workers !== undefined };
|
|
29
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {pathToFileURL} from 'node:url';
|
|
3
|
+
import {writeFile} from 'node:fs/promises';
|
|
4
|
+
import {createHash} from 'node:crypto';
|
|
5
|
+
import {localizedStaticPaths} from './i18n.mjs';
|
|
6
|
+
import {mapConcurrent} from './concurrent.mjs';
|
|
7
|
+
import {mapPrerender,prerenderPath} from './prerender-pool.mjs';
|
|
8
|
+
const isDynamic=pattern=>pattern.includes('[');
|
|
9
|
+
|
|
10
|
+
export async function prerenderPages(stage, manifest, dev) {
|
|
11
|
+
// Development renders on demand. Do not load a fresh renderer or execute
|
|
12
|
+
// getStaticProps/getStaticPaths for every route on every edit.
|
|
13
|
+
const renderer = dev ? null : await import(pathToFileURL(path.join(stage, 'runtime/render.mjs')).href);
|
|
14
|
+
const paths = new Set();
|
|
15
|
+
const validatedDocuments = new Set();
|
|
16
|
+
const staticListings = new Map();
|
|
17
|
+
const jobs=[],seedPaths=new Map(),seeds=new Map();
|
|
18
|
+
// Error page data is available when another static page returns notFound.
|
|
19
|
+
const ordered = [...manifest.routes].sort((left, right) => Number(!!right.errorStatus) - Number(!!left.errorStatus));
|
|
20
|
+
async function renderRoute(route,immediate=false) {
|
|
21
|
+
if (route.kind !== 'page' || route.router === 'app') return;
|
|
22
|
+
const modulePath = path.join(stage, route.module);
|
|
23
|
+
const page = await import(pathToFileURL(modulePath).href);
|
|
24
|
+
if (page.__prnextDocument && !validatedDocuments.has(page.__prnextDocument)) {
|
|
25
|
+
for (const name of ['getStaticProps', 'getStaticPaths', 'getServerSideProps']) {
|
|
26
|
+
if (Object.hasOwn(page.__prnextDocument, name) || typeof page.Document?.[name] === 'function') throw new Error(`pages/_document does not support ${name}. Use Document.getInitialProps for server document rendering.`);
|
|
27
|
+
}
|
|
28
|
+
validatedDocuments.add(page.__prnextDocument);
|
|
29
|
+
}
|
|
30
|
+
if (route.errorStatus && (page.getServerSideProps || page.default?.getInitialProps)) throw new Error(`pages${route.pattern} must be static and cannot use getServerSideProps or getInitialProps. Use getStaticProps instead.`);
|
|
31
|
+
route.gip = typeof page.default?.getInitialProps === 'function';
|
|
32
|
+
route.appGip = !!page.App && page.App.getInitialProps !== page.App.origGetInitialProps;
|
|
33
|
+
if (route.internal && route.errorStatus && route.appGip) {
|
|
34
|
+
// A custom App hook makes the built-in Error request-dependent. Select
|
|
35
|
+
// its actual _error module so the default status props and visible URL
|
|
36
|
+
// reach App.getInitialProps instead of a synthetic static wrapper.
|
|
37
|
+
const kind = route.errorStatus === 404 ? 'notFound' : 'serverError';
|
|
38
|
+
if (manifest.pagesErrors?.[kind] === route.id) delete manifest.pagesErrors[kind];
|
|
39
|
+
}
|
|
40
|
+
if (route.gip && (page.getStaticProps || page.getServerSideProps)) throw new Error(`Page ${route.pattern} cannot combine getInitialProps with getStaticProps or getServerSideProps.`);
|
|
41
|
+
if (route.internal && !route.errorStatus) {
|
|
42
|
+
if (page.getStaticProps || page.getServerSideProps || page.getStaticPaths) throw new Error('pages/_error does not support getStaticProps, getServerSideProps or getStaticPaths. Use ErrorComponent.getInitialProps instead.');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (page.getServerSideProps && page.getStaticProps) throw new Error(`Page ${route.pattern} exports both getServerSideProps and getStaticProps. Choose one.`);
|
|
46
|
+
if (page.getStaticPaths && !page.getStaticProps) throw new Error(`Page ${route.pattern} exports getStaticPaths without getStaticProps.`);
|
|
47
|
+
if (page.getServerSideProps) { route.ssp = true; return; }
|
|
48
|
+
if (page.getStaticProps) route.ssg = true;
|
|
49
|
+
else if (route.gip || route.appGip) return;
|
|
50
|
+
if (isDynamic(route.pattern) && page.getStaticProps && !page.getStaticPaths) throw new Error(`Dynamic page ${route.pattern} with getStaticProps must export getStaticPaths.`);
|
|
51
|
+
if (!isDynamic(route.pattern) && page.getStaticPaths) throw new Error(`getStaticPaths is only valid on dynamic pages (${route.pattern}).`);
|
|
52
|
+
// Preserve data/export validation and SSG metadata. The dev PageCache calls
|
|
53
|
+
// getStaticPaths per request, including fallback:false and locale checks.
|
|
54
|
+
if (dev) return;
|
|
55
|
+
let entries = [{ path: route.pattern, params: {} }];
|
|
56
|
+
if (isDynamic(route.pattern)) {
|
|
57
|
+
if (!page.getStaticProps) return;
|
|
58
|
+
if (!staticListings.has(modulePath)) staticListings.set(modulePath, Promise.resolve().then(() => page.getStaticPaths(manifest.config.i18n ? {locales: manifest.config.i18n.locales, defaultLocale: manifest.config.i18n.defaultLocale} : {})));
|
|
59
|
+
const result = localizedStaticPaths(route, await staticListings.get(modulePath), manifest.config.i18n);
|
|
60
|
+
route.fallback = result.fallback;
|
|
61
|
+
entries = result.paths;
|
|
62
|
+
}
|
|
63
|
+
seedPaths.set(route.id,entries.map(entry=>entry.path));
|
|
64
|
+
const routeJobs=[];
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (paths.has(entry.path)) throw new Error(`Multiple pages prerender the same path: ${entry.path}.`);
|
|
67
|
+
paths.add(entry.path);
|
|
68
|
+
routeJobs.push(async()=>{
|
|
69
|
+
const rendered = await prerenderPath('pages',{ ...manifest.config, manifest, distDir: stage, route, modulePath, path: entry.path, params: entry.params, client: route.client, css: route.css, pattern: route.pattern, errorStatus: route.errorStatus, buildId: manifest.buildId, production: !dev },renderer.prerenderRoute);
|
|
70
|
+
const file = `static/${createHash('sha256').update(entry.path).digest('hex').slice(0, 20)}.html`;
|
|
71
|
+
await writeFile(path.join(stage, file), rendered.body);
|
|
72
|
+
const seed = { routeId: route.id, path: entry.path, file, status: route.errorStatus || rendered.status || 200, headers: rendered.headers || {} };
|
|
73
|
+
if (route.ssg) {
|
|
74
|
+
seed.dataFile = file.replace(/\.html$/, '.json');
|
|
75
|
+
seed.revalidate = rendered.revalidate ?? false;
|
|
76
|
+
seed.generatedAt = rendered.generatedAt;
|
|
77
|
+
await writeFile(path.join(stage, seed.dataFile), rendered.dataJSON);
|
|
78
|
+
}
|
|
79
|
+
manifest.prerendered.push(seed);
|
|
80
|
+
seeds.set(entry.path,seed);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (route.fallback === true) {
|
|
84
|
+
routeJobs.push(async()=>{
|
|
85
|
+
const rendered = await prerenderPath('fallback',{ ...manifest.config, manifest, distDir: stage, route, modulePath, path: route.pattern, params: {}, client: route.client, css: route.css, pattern: route.pattern, buildId: manifest.buildId, production: !dev },renderer.renderFallback);
|
|
86
|
+
route.fallbackFile = `static/fallback-${route.id}.html`;
|
|
87
|
+
await writeFile(path.join(stage, route.fallbackFile), rendered.body);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if(immediate)for(const run of routeJobs)await run();
|
|
91
|
+
else jobs.push({route,run:routeJobs});
|
|
92
|
+
}
|
|
93
|
+
// Error seeds must exist before ordinary pages can render notFound/failures.
|
|
94
|
+
for(const route of ordered.filter(route=>route.errorStatus))await renderRoute(route,true);
|
|
95
|
+
await mapConcurrent(ordered.filter(route=>!route.errorStatus),route=>renderRoute(route));
|
|
96
|
+
jobs.sort((a,b)=>manifest.routes.indexOf(a.route)-manifest.routes.indexOf(b.route));
|
|
97
|
+
await mapPrerender(jobs.flatMap(job=>job.run),run=>run());
|
|
98
|
+
// Rendering errors first must not change the published route/seed ordering.
|
|
99
|
+
manifest.prerendered = manifest.routes.flatMap(route => (seedPaths.get(route.id)||[]).flatMap(path=>seeds.has(path)?[seeds.get(path)]:[]));
|
|
100
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { fork } from 'node:child_process';
|
|
3
|
+
import { prerenderResources } from './prerender-config.mjs';
|
|
4
|
+
import { createPrerenderCache } from './prerender-cache.mjs';
|
|
5
|
+
import { mapConcurrent } from './concurrent.mjs';
|
|
6
|
+
|
|
7
|
+
const storage = new AsyncLocalStorage();
|
|
8
|
+
const MIB = 1024 * 1024;
|
|
9
|
+
|
|
10
|
+
export async function withPrerenderSession(root, stage, manifest, config, callback, {signal}={}) {
|
|
11
|
+
if (storage.getStore()) return callback();
|
|
12
|
+
const session = new PrerenderPool(root, stage, manifest, config);
|
|
13
|
+
const abort=()=>void session.abort(signal.reason instanceof Error?signal.reason:new Error('Prerender cancelled'));
|
|
14
|
+
signal?.addEventListener('abort',abort,{once:true});
|
|
15
|
+
try { signal?.throwIfAborted();const result=await storage.run(session,callback);signal?.throwIfAborted();return result; }
|
|
16
|
+
finally { signal?.removeEventListener('abort',abort);await session.close(); }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function mapPrerender(items, run) {
|
|
20
|
+
const values = Array.from(items), session = storage.getStore();
|
|
21
|
+
if (!session) return mapConcurrent(values, run);
|
|
22
|
+
await session.prepare(values.length);
|
|
23
|
+
try { return await mapConcurrent(values, run, session.parallelism); }
|
|
24
|
+
catch (error) { await session.abort(error); throw error; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function prerenderPath(kind, options, local) {
|
|
28
|
+
const session = storage.getStore();
|
|
29
|
+
return session?.enabled ? session.run(kind, options) : local(options);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class PrerenderPool {
|
|
33
|
+
constructor(root, stage, manifest, config, resources, dependencies = {}) {
|
|
34
|
+
this.root = root; this.stage = stage; this.manifest = manifest;
|
|
35
|
+
this.resources = prerenderResources(config, resources);
|
|
36
|
+
this.children = []; this.enabled = false; this.parallelism = this.resources.workers;
|
|
37
|
+
this.queue = []; this.revision = 0;
|
|
38
|
+
this.sequence = 0; this.failure = undefined; this.closing = false;
|
|
39
|
+
this.fork = dependencies.fork || fork;
|
|
40
|
+
this.killGraceMs = dependencies.killGraceMs ?? 5000;
|
|
41
|
+
this.updateTimeoutMs = dependencies.updateTimeoutMs ?? 30000;
|
|
42
|
+
}
|
|
43
|
+
async prepare(count) {
|
|
44
|
+
if (this.failure) throw this.failure;
|
|
45
|
+
if(this.preparing){await this.preparing;return;}
|
|
46
|
+
// Starting a second Node/React runtime costs more than a tiny local render.
|
|
47
|
+
if(this.enabled){await this.updateManifest();return;}
|
|
48
|
+
if (this.parallelism < 2 || count < (this.resources.explicit ? 2 : 4)) return;
|
|
49
|
+
this.preparing=(async()=>{
|
|
50
|
+
this.parallelism = Math.min(this.parallelism, count);
|
|
51
|
+
this.cache = await createPrerenderCache();
|
|
52
|
+
try {
|
|
53
|
+
if(this.failure)throw this.failure;
|
|
54
|
+
await Promise.all(Array.from({ length: this.parallelism }, () => this.start()));
|
|
55
|
+
this.enabled = true;
|
|
56
|
+
} catch (error) { await this.abort(error); throw error; }
|
|
57
|
+
})();
|
|
58
|
+
try{await this.preparing;}finally{this.preparing=undefined;}
|
|
59
|
+
}
|
|
60
|
+
start() {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const child = this.fork(new URL('./prerender-process-worker.mjs', import.meta.url), [], {
|
|
63
|
+
cwd: this.root, execArgv: [`--max-old-space-size=${this.resources.heapMb}`], serialization: 'advanced',
|
|
64
|
+
env: { ...process.env, ...this.cache.env }, stdio: ['ignore', 'inherit', 'inherit', 'ipc'], windowsHide: true,
|
|
65
|
+
});
|
|
66
|
+
const worker = { child, rss: 0, busy: false, ready: false, updates: new Map() };
|
|
67
|
+
this.children.push(worker);
|
|
68
|
+
// A failed spawn emits error+close without exit. close is also the final
|
|
69
|
+
// stdio/IPC terminal event for a successfully spawned process.
|
|
70
|
+
worker.exited = new Promise(done => child.once('close', done));
|
|
71
|
+
const timeout = setTimeout(() => void this.abort(new Error('Prerender worker startup exceeded 30 seconds')), 30_000);
|
|
72
|
+
child.once('close',()=>clearTimeout(timeout));
|
|
73
|
+
worker.startReject = reject;
|
|
74
|
+
child.on('error', error => void this.abort(error));
|
|
75
|
+
child.on('exit', (code, signal) => {
|
|
76
|
+
clearTimeout(timeout);
|
|
77
|
+
if (!this.closing) void this.abort(new Error(`Prerender worker exited (${signal || code}); its path was not retried`));
|
|
78
|
+
});
|
|
79
|
+
child.on('message', message => {
|
|
80
|
+
if (Number.isSafeInteger(message.rss)) worker.rss = message.rss;
|
|
81
|
+
if (this.children.reduce((sum, value) => sum + value.rss, 0) > this.resources.maxMemoryMb * MIB) {
|
|
82
|
+
void this.abort(new Error(`Prerender workers exceeded the ${this.resources.maxMemoryMb} MiB RSS budget`)); return;
|
|
83
|
+
}
|
|
84
|
+
if (message.type === 'ready') { clearTimeout(timeout); worker.ready = true; worker.startReject = undefined; resolve(); }
|
|
85
|
+
else if(message.type==='updated'){const update=worker.updates.get(message.revision);worker.updates.delete(message.revision);clearTimeout(update?.timer);update?.resolve();}
|
|
86
|
+
else if (message.type === 'failure') { void this.abort(Object.assign(new Error(message.error), { code: message.code })); }
|
|
87
|
+
else if (message.type === 'result') {
|
|
88
|
+
if (!worker.current || worker.current.id !== message.id) { void this.abort(new Error('Invalid prerender worker response')); return; }
|
|
89
|
+
clearTimeout(worker.current.timer);
|
|
90
|
+
const current = worker.current; worker.current = undefined; worker.busy = false; current.resolve(message.rendered);this.dispatch();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
child.send({ type: 'init', stage: this.stage, manifest: this.manifest }, error => { if (error) void this.abort(error); });
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
run(kind, options) {
|
|
97
|
+
if (this.failure) return Promise.reject(this.failure);
|
|
98
|
+
if(this.closing||this.closed||!this.enabled)return Promise.reject(new Error('Prerender pool is not open'));
|
|
99
|
+
if(this.queue.length>=32)return Promise.reject(new Error('Prerender scheduler exceeded its bounded waiting queue'));
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
this.queue.push({kind,options,resolve,reject});this.dispatch();
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
dispatch(){
|
|
105
|
+
if(this.failure||this.closing)return;
|
|
106
|
+
for(const worker of this.children){
|
|
107
|
+
if(!worker.ready||worker.busy||!this.queue.length)continue;
|
|
108
|
+
const {kind,options,resolve,reject}=this.queue.shift();
|
|
109
|
+
worker.busy=true;const id=++this.sequence;
|
|
110
|
+
const timeoutMs = Math.min(300_000, (options.route?.cacheConfig?.maxDuration || 60) * 1000);
|
|
111
|
+
const timer = setTimeout(() => void this.abort(new Error(`Prerender path ${options.path} exceeded ${timeoutMs} ms; it was not retried`)), timeoutMs);
|
|
112
|
+
worker.current = { id, resolve, reject, timer };
|
|
113
|
+
const { manifest, ...serialized } = options;
|
|
114
|
+
worker.child.send({ type: 'job', id, kind, options: serialized }, error => { if (error) void this.abort(error); });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async updateManifest(){
|
|
118
|
+
const revision=++this.revision;
|
|
119
|
+
await Promise.all(this.children.map(worker=>new Promise((resolve,reject)=>{
|
|
120
|
+
const timer=setTimeout(()=>void this.abort(new Error('Prerender manifest acknowledgement timed out')),this.updateTimeoutMs);
|
|
121
|
+
worker.updates.set(revision,{resolve,reject,timer});
|
|
122
|
+
worker.child.send({type:'update',revision,manifest:this.manifest},error=>{if(error)void this.abort(error);});
|
|
123
|
+
})));
|
|
124
|
+
}
|
|
125
|
+
async abort(error) {
|
|
126
|
+
this.failure ||= error;
|
|
127
|
+
this.closing = true;
|
|
128
|
+
for(const job of this.queue.splice(0))job.reject(this.failure);
|
|
129
|
+
for (const worker of this.children) {
|
|
130
|
+
worker.startReject?.(this.failure); worker.startReject = undefined;
|
|
131
|
+
for(const update of worker.updates.values()){clearTimeout(update.timer);update.reject(this.failure);}worker.updates.clear();
|
|
132
|
+
if (worker.current) { clearTimeout(worker.current.timer); worker.current.reject(this.failure); worker.current = undefined; }
|
|
133
|
+
}
|
|
134
|
+
await this.stopChildren(true);
|
|
135
|
+
}
|
|
136
|
+
stopChildren(force) {
|
|
137
|
+
if(this.stopping)return this.stopping;
|
|
138
|
+
this.stopping=(async()=>{
|
|
139
|
+
for(const {child}of this.children){
|
|
140
|
+
if(!child.pid||child.exitCode!==null||child.signalCode!==null)continue;
|
|
141
|
+
if(force)child.kill('SIGTERM');
|
|
142
|
+
else child.send({type:'close'},()=>{});
|
|
143
|
+
}
|
|
144
|
+
// User code may install a SIGTERM handler. Arm escalation before waiting
|
|
145
|
+
// for any terminal event, including on the error/cancellation path.
|
|
146
|
+
const killer=setTimeout(()=>{
|
|
147
|
+
for(const {child}of this.children)if(child.pid&&child.exitCode===null&&child.signalCode===null)child.kill('SIGKILL');
|
|
148
|
+
},this.killGraceMs);
|
|
149
|
+
try{await Promise.all(this.children.map(worker=>worker.exited));}
|
|
150
|
+
finally{clearTimeout(killer);}
|
|
151
|
+
})();
|
|
152
|
+
return this.stopping;
|
|
153
|
+
}
|
|
154
|
+
async close() {
|
|
155
|
+
if (this.closed) return this.closed;
|
|
156
|
+
this.closed = (async () => {
|
|
157
|
+
await this.preparing?.catch(()=>{});
|
|
158
|
+
if(this.queue.length||this.children.some(worker=>worker.current))await this.abort(new Error('Prerender session closed with unfinished paths; they were not retried'));
|
|
159
|
+
this.closing = true;
|
|
160
|
+
try { await this.stopChildren(Boolean(this.failure)); }
|
|
161
|
+
finally { await this.cache?.close(); }
|
|
162
|
+
})();
|
|
163
|
+
return this.closed;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
|
|
4
|
+
let stage, manifest, busy = false;
|
|
5
|
+
let closeRuntime;
|
|
6
|
+
const send = value => new Promise((resolve, reject) => process.send(value, error => error ? reject(error) : resolve()));
|
|
7
|
+
const memory = () => ({ rss: process.memoryUsage().rss, pid: process.pid });
|
|
8
|
+
const pulse = setInterval(() => { if (process.connected) process.send({ type: 'memory', ...memory() }); }, 100);
|
|
9
|
+
pulse.unref();
|
|
10
|
+
|
|
11
|
+
process.on('message', async message => {
|
|
12
|
+
try {
|
|
13
|
+
if (message.type === 'init') {
|
|
14
|
+
if (stage) throw new Error('Prerender worker was initialized twice');
|
|
15
|
+
({ stage, manifest } = message);
|
|
16
|
+
await send({ type: 'ready', ...memory() });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (message.type === 'update') {
|
|
20
|
+
manifest = message.manifest;
|
|
21
|
+
await send({ type: 'updated', revision: message.revision });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (message.type === 'close') {
|
|
25
|
+
if (busy) throw new Error('Cannot close an active prerender worker');
|
|
26
|
+
if (closeRuntime) await closeRuntime();
|
|
27
|
+
clearInterval(pulse);
|
|
28
|
+
process.disconnect();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (message.type !== 'job' || !stage || busy) throw new Error('Invalid prerender worker operation');
|
|
32
|
+
busy = true;
|
|
33
|
+
const options = { ...message.options, manifest };
|
|
34
|
+
let rendered;
|
|
35
|
+
if (message.kind === 'pages' || message.kind === 'fallback') {
|
|
36
|
+
const runtime = await import(pathToFileURL(path.join(stage, 'runtime/render.mjs')).href);
|
|
37
|
+
rendered = await runtime[message.kind === 'pages' ? 'prerenderRoute' : 'renderFallback'](options);
|
|
38
|
+
} else if (message.kind === 'app' || message.kind === 'handler') {
|
|
39
|
+
const runtime = await import(pathToFileURL(path.join(stage, `runtime/${message.kind === 'app' ? 'app-static' : 'route-static'}.mjs`)).href);
|
|
40
|
+
if (message.kind === 'app') closeRuntime ||= (await import(pathToFileURL(path.join(stage, 'runtime/app-render.mjs')).href)).closeAppRuntime;
|
|
41
|
+
rendered = await runtime[message.kind === 'app' ? 'prerenderAppRoute' : 'prerenderAppRouteHandler'](options);
|
|
42
|
+
} else throw new Error('Unknown prerender job kind');
|
|
43
|
+
busy = false;
|
|
44
|
+
// Runtime prerender entrypoints already flush their request/cache work.
|
|
45
|
+
// Do not transfer retained finalizers, contexts or incidental worker data.
|
|
46
|
+
const result = {};
|
|
47
|
+
for (const field of ['body','dataJSON','status','headers','revalidate','generatedAt','tags','paths','partial','metadata','dynamic','reason','flight']) {
|
|
48
|
+
if (rendered[field] !== undefined) result[field] = rendered[field];
|
|
49
|
+
}
|
|
50
|
+
await send({ type: 'result', id: message.id, rendered: result, ...memory() });
|
|
51
|
+
} catch (error) {
|
|
52
|
+
busy = false;
|
|
53
|
+
await send({ type: 'failure', id: message.id, error: error.stack || error.message, code: error.code, ...memory() }).catch(() => {});
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// A dead coordinator must not leave application work or an RSC thread behind.
|
|
58
|
+
process.once('disconnect', () => process.exit(0));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {prerenderProduction} from './production-builder.mjs';
|
|
2
|
+
const controller=new AbortController();
|
|
3
|
+
process.on('message',message=>{if(message.type==='abort')controller.abort(new Error('Production prerender cancelled by supervisor'));});
|
|
4
|
+
process.once('message',async({root,stage,manifest,options})=>{
|
|
5
|
+
try{
|
|
6
|
+
await prerenderProduction(root,stage,manifest,{...options,signal:controller.signal});
|
|
7
|
+
process.send({ok:true,manifest},()=>process.exit(0));
|
|
8
|
+
}catch(error){process.send({ok:false,error:error.stack||error.message},()=>process.exit(1));}
|
|
9
|
+
});
|
|
10
|
+
process.once('disconnect',()=>process.exit(1));
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {fork} from 'node:child_process';
|
|
2
|
+
import {readFile,realpath} from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {watchProject} from '../runtime/watch-project.mjs';
|
|
5
|
+
import {createDevChangeFilter} from '../runtime/dev-file-changes.mjs';
|
|
6
|
+
import {incrementalCompilerStats} from './incremental.mjs';
|
|
7
|
+
import {withPrerenderSession} from './prerender-pool.mjs';
|
|
8
|
+
import {prerenderPages} from './prerender-pages.mjs';
|
|
9
|
+
import {prerenderApp} from './app-static.mjs';
|
|
10
|
+
|
|
11
|
+
/** Explicit reusable build session for integrations; always dispose it. */
|
|
12
|
+
export async function createBuilder(projectRoot,options={}){
|
|
13
|
+
const root=await realpath(projectRoot);let disposed=false,pending=Promise.resolve();
|
|
14
|
+
return {
|
|
15
|
+
build(overrides={}){
|
|
16
|
+
if(disposed)return Promise.reject(new Error('Builder has been disposed'));
|
|
17
|
+
const task=pending.then(async()=>{const {build}=await import('./index.mjs');return build(root,{...options,...overrides,dev:false,incremental:true});});
|
|
18
|
+
pending=task.catch(()=>{});return task;
|
|
19
|
+
},
|
|
20
|
+
stats:incrementalCompilerStats,
|
|
21
|
+
async dispose(){disposed=true;await pending;const {disposeBuildSession}=await import('./index.mjs');await disposeBuildSession(root);},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Application imports, SSG and RSC isolates belong to one build only. Keeping
|
|
26
|
+
// esbuild's graph must not retain application globals or execute stale modules.
|
|
27
|
+
export async function prerenderProduction(root,stage,manifest,{prerender,signal}={}){
|
|
28
|
+
await withPrerenderSession(root,stage,manifest,prerender,async()=>{
|
|
29
|
+
await prerenderPages(stage,manifest,false);
|
|
30
|
+
await prerenderApp(stage,manifest,false);
|
|
31
|
+
},{signal});
|
|
32
|
+
return manifest;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isolatedPrerender(root, stage, manifest, options = {}, dependencies = {}) {
|
|
36
|
+
const spawn = dependencies.fork || fork;
|
|
37
|
+
const { timeoutMs = 600000, abortGraceMs = 30000, killGraceMs = 5000 } = dependencies;
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const child = spawn(new URL('./prerender-worker.mjs', import.meta.url), [], {
|
|
40
|
+
cwd: root, execArgv: [], stdio: ['ignore', 'inherit', 'inherit', 'ipc'], windowsHide: true,
|
|
41
|
+
});
|
|
42
|
+
let result, failure, terminateTimer, killTimer, closed = false, terminating = false;
|
|
43
|
+
const running = () => !closed && child.pid && child.exitCode === null && child.signalCode === null;
|
|
44
|
+
const terminate = () => {
|
|
45
|
+
if (!running() || terminating) return;
|
|
46
|
+
terminating = true;
|
|
47
|
+
child.kill('SIGTERM');
|
|
48
|
+
// SIGTERM is an application-observable signal on POSIX. Never wait on
|
|
49
|
+
// its handler before arming the unconditional final termination.
|
|
50
|
+
killTimer ||= setTimeout(() => { if (running()) child.kill('SIGKILL'); }, killGraceMs);
|
|
51
|
+
};
|
|
52
|
+
const fail = error => {
|
|
53
|
+
failure ||= error;
|
|
54
|
+
clearTimeout(timeout);
|
|
55
|
+
clearTimeout(terminateTimer);
|
|
56
|
+
terminate();
|
|
57
|
+
};
|
|
58
|
+
const send = message => {
|
|
59
|
+
if (!child.connected) { fail(new Error('Prerender coordinator IPC disconnected')); return; }
|
|
60
|
+
try { child.send(message, error => { if (error && !closed) fail(error); }); }
|
|
61
|
+
catch (error) { fail(error); }
|
|
62
|
+
};
|
|
63
|
+
const timeout = setTimeout(() => {
|
|
64
|
+
failure ||= new Error('Production prerender exceeded ten minutes');
|
|
65
|
+
// Cooperative cancellation drains the coordinator's owned path workers.
|
|
66
|
+
// A blocked application generator may never process this IPC message.
|
|
67
|
+
send({ type: 'abort' });
|
|
68
|
+
if (!closed) terminateTimer = setTimeout(terminate, abortGraceMs);
|
|
69
|
+
}, timeoutMs);
|
|
70
|
+
child.on('error', fail);
|
|
71
|
+
child.once('disconnect', () => { if (!closed && !result) fail(new Error('Prerender coordinator IPC disconnected before completion')); });
|
|
72
|
+
child.on('message', value => { if (typeof value?.ok === 'boolean') result = value; });
|
|
73
|
+
// Failed spawns emit error+close without exit. Publish/reject only after
|
|
74
|
+
// the final terminal event, so stage cleanup cannot race this process.
|
|
75
|
+
child.once('close', (code, signal) => {
|
|
76
|
+
closed = true;
|
|
77
|
+
clearTimeout(timeout); clearTimeout(terminateTimer); clearTimeout(killTimer);
|
|
78
|
+
if (!failure && result?.ok && code === 0) resolve(result.manifest);
|
|
79
|
+
else reject(failure || new Error(result?.error || `Prerender exited ${signal || code}`));
|
|
80
|
+
});
|
|
81
|
+
send({ root, stage, manifest, options });
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Retain production graphs during local rebuilds; no HTTP listener/daemon. */
|
|
86
|
+
export async function watchProduction(root,options={}){
|
|
87
|
+
let worker,watcher,stopping=false,timer,busy=false,queued=false;
|
|
88
|
+
const changed=new Set(),filter=createDevChangeFilter(root);
|
|
89
|
+
let output='.prnext';
|
|
90
|
+
const stopWorker=async()=>{
|
|
91
|
+
const old=worker;worker=undefined;if(!old||old.exitCode!==null||old.signalCode)return;
|
|
92
|
+
await new Promise(resolve=>{const timer=setTimeout(()=>old.kill(),5000);old.once('exit',()=>{clearTimeout(timer);resolve();});old.disconnect();});
|
|
93
|
+
};
|
|
94
|
+
const shutdown=async()=>{if(stopping)return;stopping=true;clearTimeout(timer);watcher?.close();await stopWorker();};
|
|
95
|
+
process.once('SIGINT',shutdown);process.once('SIGTERM',shutdown);
|
|
96
|
+
async function rebuild(){
|
|
97
|
+
if(stopping)return;if(busy){queued=true;return;}busy=true;
|
|
98
|
+
const pending=[...changed];changed.clear();
|
|
99
|
+
try{
|
|
100
|
+
const actual=await filter(pending);
|
|
101
|
+
if(worker&&!actual.length)return;
|
|
102
|
+
if(actual.some(file=>/(?:^|\/)(?:(?:next|prnext|contentlayer)\.config\.|package(?:-lock)?\.json$|pnpm-lock\.yaml$|yarn\.lock$|\.env)/.test(file)))await stopWorker();
|
|
103
|
+
if(!worker){worker=fork(new URL('./dev-worker.mjs',import.meta.url),[],{cwd:root,execArgv:[],stdio:['inherit','inherit','inherit','ipc'],windowsHide:true});}
|
|
104
|
+
const current=worker;
|
|
105
|
+
const result=await new Promise((resolve,reject)=>{
|
|
106
|
+
const cleanup=()=>{current.off('message',message);current.off('exit',exit);current.off('error',error);};
|
|
107
|
+
const message=value=>{if(value?.type==='result'){cleanup();resolve(value);}};
|
|
108
|
+
const exit=()=>{cleanup();reject(new Error('Production compiler exited'));};
|
|
109
|
+
const error=value=>{cleanup();reject(value);};
|
|
110
|
+
current.on('message',message);current.once('exit',exit);current.once('error',error);
|
|
111
|
+
current.send({type:'build',root,options:{...options,dev:false,incremental:true}});
|
|
112
|
+
});
|
|
113
|
+
if(result.recycle)await stopWorker();
|
|
114
|
+
if(!result.ok)throw new Error(result.error);
|
|
115
|
+
filter.acceptInputs(result.inputs||[]);
|
|
116
|
+
try{output=JSON.parse(await readFile(path.join(root,'.prnext-output.json'),'utf8')).distDir||output;}catch{}
|
|
117
|
+
await watcher?.refresh();
|
|
118
|
+
console.log(`Watching production sources (${result.compiler?.reused||0} compiler graph reuse(s)).`);
|
|
119
|
+
}catch(error){console.error(error.stack||error);}
|
|
120
|
+
finally{busy=false;if(queued&&!stopping){queued=false;void rebuild();}}
|
|
121
|
+
}
|
|
122
|
+
try{
|
|
123
|
+
watcher=await watchProject(root,{ignore:relative=>relative===output||relative.startsWith(output+'/'),onError:error=>{console.error(error);process.exitCode=1;void shutdown();},onChange:file=>{changed.add(String(file).replaceAll(path.sep,'/'));clearTimeout(timer);timer=setTimeout(()=>void rebuild(),150);}});
|
|
124
|
+
await rebuild();
|
|
125
|
+
// The watcher owns the event-loop lifetime. Stop signals close it and the
|
|
126
|
+
// compiler child; builds continue to publish through the existing lock.
|
|
127
|
+
await new Promise(resolve=>{const poll=setInterval(()=>{if(stopping&&!worker){clearInterval(poll);resolve();}},100);});
|
|
128
|
+
}finally{await shutdown();process.off('SIGINT',shutdown);process.off('SIGTERM',shutdown);}
|
|
129
|
+
}
|