@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
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
2
|
+
import { request } from 'node:http';
|
|
3
3
|
import { cp, mkdir, readFile, rm, realpath } from 'node:fs/promises';
|
|
4
4
|
import { randomUUID, createHash } from 'node:crypto';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { readBuildDirectory } from '../build-directory.mjs';
|
|
7
7
|
import { atomicJSON, delay, environment, validName, validHealthPath } from './common.mjs';
|
|
8
8
|
import { RotatingLog } from './logger.mjs';
|
|
9
|
+
import {nativeGateway,stopNativeProxy} from './native-proxy.mjs';
|
|
9
10
|
|
|
10
11
|
const home = process.argv[2];
|
|
11
12
|
const registry = path.join(home, 'apps.json');
|
|
@@ -16,6 +17,7 @@ const send = message => { if (process.connected) process.send(message); };
|
|
|
16
17
|
|
|
17
18
|
function validate(config) {
|
|
18
19
|
validName(config.name); validHealthPath(config.healthPath);
|
|
20
|
+
if(config.rollbackWindow!==undefined&&(!Number.isInteger(config.rollbackWindow)||config.rollbackWindow<0||config.rollbackWindow>120000))throw new Error('rollbackWindow must be between 0 and 120000 milliseconds');
|
|
19
21
|
if (!path.isAbsolute(config.root) || !path.isAbsolute(config.binary) || !path.isAbsolute(config.node)) throw new Error('Application, Node and native executable paths must be absolute');
|
|
20
22
|
if (!Number.isInteger(config.port) || config.port < 1 || config.port > 65535) throw new Error('--port must be between 1 and 65535');
|
|
21
23
|
if (typeof config.hostname !== 'string' || !config.hostname || /[\s\x00-\x1f]/.test(config.hostname)) throw new Error('Choose a valid --hostname');
|
|
@@ -27,7 +29,8 @@ async function persist() {
|
|
|
27
29
|
await atomicJSON(registry, { version: 1, apps: [...apps.values()].map(app => ({ config: app.config, enabled: app.enabled, snapshots: [...app.snapshots] })) });
|
|
28
30
|
}
|
|
29
31
|
function newApp(config, enabled = false) {
|
|
30
|
-
|
|
32
|
+
config={rollbackWindow:15000,...config};
|
|
33
|
+
return { config, enabled, snapshots: new Set(), active: null, generations: new Set(), gateway: null, state: 'stopped', error: null, starts: 0, failures: [], retry: null, probation:null,
|
|
31
34
|
log: new RotatingLog(path.join(home, 'logs', `app-${config.name}.log`)) };
|
|
32
35
|
}
|
|
33
36
|
function summary(app) {
|
|
@@ -36,44 +39,13 @@ function summary(app) {
|
|
|
36
39
|
activeRequests: [...app.generations].reduce((sum, generation) => sum + generation.active, 0),
|
|
37
40
|
draining: [...app.generations].filter(generation => generation !== app.active).length,
|
|
38
41
|
buildId: app.active?.buildId || null, startedAt: app.active?.startedAt || null, error: app.error,
|
|
42
|
+
proxyPid:app.gateway?.pid||null,
|
|
43
|
+
rollback:app.probation?{previousBuildId:app.probation.previous.buildId,until:app.probation.deadline}:null,
|
|
39
44
|
log: path.join(home, 'logs', `app-${app.config.name}.log`) };
|
|
40
45
|
}
|
|
41
|
-
function hopHeaders(headers) {
|
|
42
|
-
const result = { ...headers };
|
|
43
|
-
const nominated = String(headers.connection || '').split(',').map(value => value.trim().toLowerCase());
|
|
44
|
-
for (const key of ['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade', ...nominated]) delete result[key];
|
|
45
|
-
return result;
|
|
46
|
-
}
|
|
47
46
|
async function listen(app) {
|
|
48
47
|
if (app.gateway) return;
|
|
49
|
-
|
|
50
|
-
const generation = app.active;
|
|
51
|
-
if (!incoming.url.startsWith('/')) { outgoing.writeHead(400).end('Use a local request path.'); return; }
|
|
52
|
-
if (!generation || generation.dead) { outgoing.writeHead(503, { 'retry-after': '1' }).end('Application is restarting.'); incoming.resume(); return; }
|
|
53
|
-
if (generation.active >= 512) { outgoing.writeHead(503, { 'retry-after': '1' }).end('Application is busy.'); incoming.resume(); return; }
|
|
54
|
-
generation.active++;
|
|
55
|
-
const headers = hopHeaders(incoming.headers);
|
|
56
|
-
// Preserve original Host and trusted reverse-proxy headers. Never invent
|
|
57
|
-
// retry/replay behavior for POSTs or requests that have already streamed.
|
|
58
|
-
const upstream = request({ host: '127.0.0.1', port: generation.port, method: incoming.method, path: incoming.url, headers, agent: generation.agent }, response => {
|
|
59
|
-
outgoing.writeHead(response.statusCode, hopHeaders(response.headers));
|
|
60
|
-
response.on('error', () => outgoing.destroy()); response.pipe(outgoing);
|
|
61
|
-
});
|
|
62
|
-
generation.requests.add(upstream);
|
|
63
|
-
let released = false;
|
|
64
|
-
const release = () => { if (released) return; released = true; generation.active--; generation.requests.delete(upstream); upstream.destroy(); };
|
|
65
|
-
outgoing.once('finish', release); outgoing.once('close', release);
|
|
66
|
-
incoming.once('aborted', () => outgoing.destroy());
|
|
67
|
-
incoming.once('error', () => outgoing.destroy());
|
|
68
|
-
upstream.once('error', () => { if (!outgoing.headersSent) outgoing.writeHead(502).end('Application connection closed.'); else outgoing.destroy(); });
|
|
69
|
-
incoming.pipe(upstream);
|
|
70
|
-
});
|
|
71
|
-
server.on('upgrade', (_request, socket) => socket.end('HTTP/1.1 501 Not Implemented\r\nConnection: close\r\nContent-Length: 0\r\n\r\n'));
|
|
72
|
-
server.on('connection', socket => { app.sockets.add(socket); socket.once('close', () => app.sockets.delete(socket)); });
|
|
73
|
-
server.maxConnections = 1024;
|
|
74
|
-
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(app.config.port, app.config.hostname, resolve); });
|
|
75
|
-
server.on('error', error => { app.error = error.message; app.log.message(error.message); });
|
|
76
|
-
app.gateway = server;
|
|
48
|
+
app.gateway=await nativeGateway(app.config,{home,notify:send,onFatal:error=>{console.error(error.message);if(!closing)process.exit(1);}});
|
|
77
49
|
}
|
|
78
50
|
async function removeSnapshot(app, relative) {
|
|
79
51
|
// Only delete a directory generated by this manager inside this exact root.
|
|
@@ -120,11 +92,9 @@ async function stopGeneration(app, generation, drain = true) {
|
|
|
120
92
|
generation.expected = true;
|
|
121
93
|
generation.stopping = (async () => {
|
|
122
94
|
const deadline = Date.now() + (drain ? app.config.drainTimeout : 0);
|
|
123
|
-
|
|
124
|
-
for (const req of generation.requests) req.destroy();
|
|
125
|
-
generation.agent.destroy();
|
|
95
|
+
await app.gateway?.retire(generation,drain?Math.max(0,deadline-Date.now()):0).catch(error=>app.log.message(`Proxy retirement: ${error.message}`));
|
|
126
96
|
generation.child.stdin?.end();
|
|
127
|
-
await Promise.race([generation.closed, delay(3000)]);
|
|
97
|
+
await Promise.race([generation.closed, delay(drain?Math.max(1000,deadline-Date.now()):3000)]);
|
|
128
98
|
if (!generation.dead) generation.child.kill('SIGKILL');
|
|
129
99
|
if (process.platform !== 'win32' && generation.child.pid) { try { process.kill(-generation.child.pid, 'SIGKILL'); } catch {} }
|
|
130
100
|
await generation.closed;
|
|
@@ -156,7 +126,7 @@ async function launch(app) {
|
|
|
156
126
|
env: { ...environment(), NODE_ENV: 'production', ...(config.inspect ? { PRNEXT_INSPECT_DIR: path.join(config.root, '.prnext-cache/inspect') } : {}) },
|
|
157
127
|
});
|
|
158
128
|
child.stdin.on('error', () => {});
|
|
159
|
-
const generation = { child, ...build, active: 0,
|
|
129
|
+
const generation = { child, ...build, active: 0, dead: false, expected: false, port: null, startedAt: Date.now() };
|
|
160
130
|
app.generations.add(generation);
|
|
161
131
|
if (child.pid) send({ type: 'child', pid: child.pid, running: true });
|
|
162
132
|
let output = '', failure;
|
|
@@ -173,12 +143,12 @@ async function launch(app) {
|
|
|
173
143
|
generation.closed = new Promise(resolve => child.once('close', (code, signal) => {
|
|
174
144
|
generation.dead = true;
|
|
175
145
|
if (child.pid) send({ type: 'child', pid: child.pid, running: false });
|
|
176
|
-
if (app.active === generation) app.active = null;
|
|
146
|
+
if (app.active === generation) {app.active = null;void app.gateway?.pause(generation).catch(error=>app.log.message(error.message));}
|
|
177
147
|
if (!generation.expected) {
|
|
178
148
|
app.log.message(`Process ${child.pid || 'unknown'} exited (${signal || code}).`);
|
|
179
149
|
if (generation.ready) {
|
|
180
150
|
// Serialize recovery and cleanup with explicit lifecycle commands.
|
|
181
|
-
sequence = sequence.then(async () => { await stopGeneration(app, generation, false); await persist(); retry(app, new Error(`Application exited (${signal || code})`)); }).catch(error => app.log.message(error.stack));
|
|
151
|
+
sequence = sequence.then(async () => { if(app.probation?.next===generation&&await rollback(app,new Error(`Replacement exited (${signal||code})`)))return;await stopGeneration(app, generation, false); await persist(); if(!app.active)retry(app, new Error(`Application exited (${signal || code})`)); }).catch(error => app.log.message(error.stack));
|
|
182
152
|
}
|
|
183
153
|
}
|
|
184
154
|
resolve();
|
|
@@ -197,7 +167,42 @@ async function launch(app) {
|
|
|
197
167
|
} catch (error) { await stopGeneration(app, generation, false); throw error; }
|
|
198
168
|
finally { child.stdout.off('data', started); }
|
|
199
169
|
}
|
|
170
|
+
async function finishProbation(app){
|
|
171
|
+
const probation=app.probation;if(!probation)return;
|
|
172
|
+
clearTimeout(probation.timer);app.probation=null;
|
|
173
|
+
await stopGeneration(app,probation.previous).catch(error=>app.log.message(`Previous build cleanup: ${error.message}`));
|
|
174
|
+
await persist();
|
|
175
|
+
}
|
|
176
|
+
async function rollback(app,error){
|
|
177
|
+
const probation=app.probation;if(!probation)return false;
|
|
178
|
+
clearTimeout(probation.timer);app.probation=null;
|
|
179
|
+
if(!app.enabled||closing||probation.previous.dead||probation.previous.expected){
|
|
180
|
+
await stopGeneration(app,probation.previous,false);return false;
|
|
181
|
+
}
|
|
182
|
+
// A live previous process still has to pass its health check. Never roll
|
|
183
|
+
// traffic back to an instance whose dependencies have also become unhealthy.
|
|
184
|
+
try{await health(probation.previous,app.config,Math.min(2000,app.config.healthTimeout));await app.gateway.activate(probation.previous);}
|
|
185
|
+
catch(restore){app.log.message(`Rollback unavailable: ${restore.message}`);await stopGeneration(app,probation.previous,false);return false;}
|
|
186
|
+
app.active=probation.previous;app.state='online';app.error=`Rolled back: ${error.message}`;
|
|
187
|
+
app.log.message(`Rolled back to build ${app.active.buildId}: ${error.message}`);
|
|
188
|
+
await stopGeneration(app,probation.next);await persist();return true;
|
|
189
|
+
}
|
|
190
|
+
function beginProbation(app,next,previous){
|
|
191
|
+
const probation=app.probation={next,previous,deadline:Date.now()+app.config.rollbackWindow,failures:0};
|
|
192
|
+
const schedule=()=>{probation.timer=setTimeout(()=>{
|
|
193
|
+
sequence=sequence.then(async()=>{
|
|
194
|
+
if(app.probation!==probation||!app.enabled||closing)return;
|
|
195
|
+
if(next.dead){await rollback(app,new Error('Replacement exited during monitoring'));return;}
|
|
196
|
+
if(Date.now()>=probation.deadline){await finishProbation(app);return;}
|
|
197
|
+
try{await health(next,app.config,Math.min(2000,app.config.healthTimeout));probation.failures=0;}
|
|
198
|
+
catch(error){if(++probation.failures>=3){if(!await rollback(app,error)){await stopGeneration(app,next,false);retry(app,error);}return;}}
|
|
199
|
+
if(app.probation===probation)schedule();
|
|
200
|
+
}).catch(error=>app.log.message(`Replacement monitoring: ${error.stack||error}`));
|
|
201
|
+
},Math.min(1000,Math.max(1,probation.deadline-Date.now())));};
|
|
202
|
+
schedule();
|
|
203
|
+
}
|
|
200
204
|
async function activate(app) {
|
|
205
|
+
await finishProbation(app);
|
|
201
206
|
await listen(app);
|
|
202
207
|
const previous = app.active;
|
|
203
208
|
const wasEnabled = app.enabled;
|
|
@@ -208,9 +213,13 @@ async function activate(app) {
|
|
|
208
213
|
app.enabled = true;
|
|
209
214
|
await persist();
|
|
210
215
|
if (next.dead) throw new Error('Application exited before traffic could switch');
|
|
216
|
+
await app.gateway.activate(next);
|
|
217
|
+
if (next.dead) throw new Error('Application exited while traffic switched');
|
|
211
218
|
app.active = next; app.state = 'online'; app.error = null;
|
|
212
219
|
app.log.message(`Ready: pid ${next.child.pid}, build ${next.buildId || 'unknown'}, ${app.config.hostname}:${app.config.port}`);
|
|
213
220
|
} catch (error) {
|
|
221
|
+
if (previous && !previous.dead) await app.gateway.activate(previous).catch(restore=>app.log.message(restore.message));
|
|
222
|
+
else await app.gateway?.pause().catch(restore=>app.log.message(restore.message));
|
|
214
223
|
if (next) await stopGeneration(app, next, false).catch(cleanup => app.log.message(cleanup.message));
|
|
215
224
|
app.enabled = wasEnabled;
|
|
216
225
|
app.state = previous && !previous.dead ? 'online' : 'errored'; app.error = error.message;
|
|
@@ -218,21 +227,17 @@ async function activate(app) {
|
|
|
218
227
|
app.log.message(`Start failed; ${previous && !previous.dead ? 'previous generation remains active' : 'no healthy generation'}: ${error.message}`);
|
|
219
228
|
throw error;
|
|
220
229
|
}
|
|
221
|
-
if (previous)
|
|
222
|
-
|
|
223
|
-
await persist();
|
|
224
|
-
}
|
|
230
|
+
if(previous && app.config.rollbackWindow>0)beginProbation(app,next,previous);
|
|
231
|
+
else if(previous){await stopGeneration(app,previous);await persist();}
|
|
225
232
|
}
|
|
226
233
|
async function stopApp(app) {
|
|
234
|
+
if(app.probation)clearTimeout(app.probation.timer);app.probation=null;
|
|
227
235
|
app.enabled = false; clearTimeout(app.retry); app.retry = null;
|
|
228
236
|
await persist();
|
|
229
237
|
app.state = 'stopping'; app.active = null;
|
|
230
|
-
|
|
231
|
-
const closed = gateway ? new Promise(resolve => gateway.close(resolve)) : Promise.resolve();
|
|
232
|
-
gateway?.closeIdleConnections();
|
|
238
|
+
await app.gateway?.pause();
|
|
233
239
|
await Promise.all([...app.generations].map(generation => stopGeneration(app, generation)));
|
|
234
|
-
|
|
235
|
-
await closed; app.state = 'stopped'; app.error = null; await persist();
|
|
240
|
+
await app.gateway?.close();app.gateway=null;app.state = 'stopped'; app.error = null; await persist();
|
|
236
241
|
app.log.message('Stopped.');
|
|
237
242
|
}
|
|
238
243
|
function select(message) {
|
|
@@ -242,8 +247,12 @@ function select(message) {
|
|
|
242
247
|
return selected;
|
|
243
248
|
}
|
|
244
249
|
async function command(message) {
|
|
245
|
-
if (message.command === 'status')
|
|
250
|
+
if (message.command === 'status') {
|
|
251
|
+
for(const app of apps.values())if(app.gateway){const counts=await app.gateway.counts();for(const g of app.generations)g.active=counts[g.relative]||0;}
|
|
252
|
+
return { daemonPid: process.pid, apps: [...apps.values()].map(summary) };
|
|
253
|
+
}
|
|
246
254
|
if (message.command === 'start') {
|
|
255
|
+
message.config={rollbackWindow:15000,...message.config};
|
|
247
256
|
validate(message.config);
|
|
248
257
|
message.config.root = await realpath(message.config.root);
|
|
249
258
|
let app = apps.get(message.config.name);
|
|
@@ -274,12 +283,13 @@ async function shutdown() {
|
|
|
274
283
|
await sequence;
|
|
275
284
|
// A supervisor shutdown preserves each enabled flag for the next launch.
|
|
276
285
|
await Promise.all([...apps.values()].map(async app => {
|
|
277
|
-
clearTimeout(app.
|
|
286
|
+
if(app.probation)clearTimeout(app.probation.timer);app.probation=null;
|
|
287
|
+
clearTimeout(app.retry); app.active = null;await app.gateway?.pause();
|
|
278
288
|
await Promise.all([...app.generations].map(generation => stopGeneration(app, generation)));
|
|
279
|
-
|
|
289
|
+
await app.gateway?.close();app.gateway=null;
|
|
280
290
|
app.log.end();
|
|
281
291
|
}));
|
|
282
|
-
await persist(); process.exit(0);
|
|
292
|
+
await stopNativeProxy();await persist(); process.exit(0);
|
|
283
293
|
}
|
|
284
294
|
process.on('disconnect', () => void shutdown());
|
|
285
295
|
process.on('SIGTERM', () => void shutdown());
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Lifecycle RPC only: no public HTTP bytes, headers or response bodies enter JS.
|
|
2
|
+
import {spawn} from 'node:child_process';
|
|
3
|
+
import {environment,delay} from './common.mjs';
|
|
4
|
+
let manager;
|
|
5
|
+
class NativeProxy {
|
|
6
|
+
constructor(binary,{home,notify,onFatal}){
|
|
7
|
+
this.pending=new Map();this.next=0;this.stopping=false;
|
|
8
|
+
this.child=spawn(binary,['persistent-proxy'],{cwd:home,windowsHide:true,detached:process.platform!=='win32',stdio:['pipe','pipe','inherit'],env:{...environment(),TOKIO_WORKER_THREADS:'1'}});
|
|
9
|
+
this.pid=this.child.pid;if(this.pid)notify({type:'child',pid:this.pid,running:true});
|
|
10
|
+
this.closed=new Promise(resolve=>this.child.once('close',()=>{if(this.pid)notify({type:'child',pid:this.pid,running:false});resolve();}));
|
|
11
|
+
this.ready=new Promise((resolve,reject)=>{
|
|
12
|
+
const timer=setTimeout(()=>{reject(new Error('Native persistent proxy did not initialize'));this.child.kill();},10000);timer.unref();
|
|
13
|
+
let buffer='';
|
|
14
|
+
this.child.stdout.setEncoding('utf8');
|
|
15
|
+
this.child.stdout.on('data',chunk=>{
|
|
16
|
+
buffer+=chunk;
|
|
17
|
+
if(buffer.length>1024*1024){this.child.kill();return;}
|
|
18
|
+
for(let end;(end=buffer.indexOf('\n'))>=0;){
|
|
19
|
+
const line=buffer.slice(0,end);buffer=buffer.slice(end+1);let message;
|
|
20
|
+
try{message=JSON.parse(line);}catch{this.child.kill();return;}
|
|
21
|
+
if(message.type==='ready'&&message.protocol===1){clearTimeout(timer);resolve();continue;}
|
|
22
|
+
const request=this.pending.get(message.id);if(!request)continue;
|
|
23
|
+
this.pending.delete(message.id);clearTimeout(request.timer);message.ok?request.resolve(message.value):request.reject(new Error(message.error||'Native proxy request failed'));
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
const failed=error=>{
|
|
27
|
+
clearTimeout(timer);reject(error);
|
|
28
|
+
for(const request of this.pending.values()){clearTimeout(request.timer);request.reject(error);}this.pending.clear();
|
|
29
|
+
if(!this.stopping)onFatal(error);
|
|
30
|
+
};
|
|
31
|
+
this.child.once('error',failed);this.child.once('exit',(code,signal)=>failed(new Error(`Native persistent proxy exited (${signal||code})`)));
|
|
32
|
+
});
|
|
33
|
+
this.child.stdin.on('error',()=>{});
|
|
34
|
+
}
|
|
35
|
+
async request(command,values={},timeout=10000){
|
|
36
|
+
await this.ready;
|
|
37
|
+
if(this.stopping||this.child.exitCode!==null||this.pending.size>=32)throw new Error('Native persistent proxy unavailable');
|
|
38
|
+
const id=++this.next;
|
|
39
|
+
return new Promise((resolve,reject)=>{
|
|
40
|
+
const timer=setTimeout(()=>{
|
|
41
|
+
this.pending.delete(id);reject(new Error('Native proxy control request timed out'));
|
|
42
|
+
// A timed-out switch/listen might still mutate native state later.
|
|
43
|
+
// End that uncertain control plane; the watchdog restores saved apps.
|
|
44
|
+
this.child.kill('SIGKILL');
|
|
45
|
+
},timeout);timer.unref();
|
|
46
|
+
this.pending.set(id,{resolve,reject,timer});
|
|
47
|
+
this.child.stdin.write(JSON.stringify({id,command,...values})+'\n',error=>{if(error){const request=this.pending.get(id);if(request){clearTimeout(request.timer);this.pending.delete(id);request.reject(error);}}});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async stop(){this.stopping=true;this.child.stdin.end();await Promise.race([this.closed,delay(3000)]);if(this.child.exitCode===null)this.child.kill('SIGKILL');await this.closed;}
|
|
51
|
+
}
|
|
52
|
+
export async function nativeGateway(config,options){
|
|
53
|
+
manager??=new NativeProxy(config.binary,options);await manager.ready;
|
|
54
|
+
const app=config.name;await manager.request('listen',{app,hostname:config.hostname,port:config.port});
|
|
55
|
+
return {pid:manager.pid,
|
|
56
|
+
activate:g=>manager.request('activate',{app,generation:g.relative,port:g.port}),
|
|
57
|
+
pause:g=>manager.request('pause',{app,...(g?{generation:g.relative}:{})}),
|
|
58
|
+
retire:(g,timeout)=>manager.request('retire',{app,generation:g.relative,timeout},timeout+5000),
|
|
59
|
+
counts:async()=> (await manager.request('status',{app})).generations,
|
|
60
|
+
close:()=>manager.request('close',{app}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export async function stopNativeProxy(){if(manager){const active=manager;manager=undefined;await active.stop();}}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const INTENT = 1;
|
|
2
|
+
const abortError = () => Object.assign(new Error('Speculative navigation cancelled'), { name: 'AbortError' });
|
|
3
|
+
|
|
4
|
+
// The completed-result cache obeys the same priority as the producer queue.
|
|
5
|
+
// Otherwise a viewport scan could evict the hovered route immediately after
|
|
6
|
+
// the scheduler had correctly protected its request.
|
|
7
|
+
export function prefetchCacheVictim(cache, priority) {
|
|
8
|
+
let candidate;
|
|
9
|
+
for (const [key, entry] of cache) {
|
|
10
|
+
if (entry.abort.signal.aborted) return key;
|
|
11
|
+
if (!candidate || entry.priority < candidate[1].priority) candidate = [key, entry];
|
|
12
|
+
}
|
|
13
|
+
return candidate && candidate[1].priority <= priority ? candidate[0] : undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Speculative work never competes with a committed navigation. The same policy
|
|
17
|
+
// is used by both routers; data and authorization remain in their own caches.
|
|
18
|
+
export function createPrefetchScheduler({ window: win = globalThis.window, document: doc = globalThis.document,
|
|
19
|
+
concurrency = 2, limit = 8 } = {}) {
|
|
20
|
+
const tasks = new Set();
|
|
21
|
+
let serial = 0, active = 0, navigating = 0, disposed = false, scheduled = false;
|
|
22
|
+
const connection = win?.navigator?.connection;
|
|
23
|
+
const priorityOf = value => value === 'viewport' ? 0 : INTENT;
|
|
24
|
+
function policy(priority) {
|
|
25
|
+
const network = win?.navigator?.connection;
|
|
26
|
+
if (doc?.visibilityState === 'hidden' || win?.navigator?.onLine === false || network?.saveData || /^(slow-)?2g$/.test(network?.effectiveType || '')) return 0;
|
|
27
|
+
const slow = network?.effectiveType === '3g' || (network?.downlink > 0 && network.downlink < 1.5);
|
|
28
|
+
return slow ? (priority < INTENT ? 0 : 1) : concurrency;
|
|
29
|
+
}
|
|
30
|
+
function finish(task, error, value) {
|
|
31
|
+
if (!tasks.delete(task)) return;
|
|
32
|
+
if (task.running) active--;
|
|
33
|
+
task.abort.signal.removeEventListener('abort', task.cancelled);
|
|
34
|
+
const { resolve, reject } = task;
|
|
35
|
+
task.run = task.resolve = task.reject = task.cancelled = undefined;
|
|
36
|
+
error ? reject(error) : resolve(value);
|
|
37
|
+
wake();
|
|
38
|
+
}
|
|
39
|
+
function start(task) {
|
|
40
|
+
task.running = true; active++;
|
|
41
|
+
let result;
|
|
42
|
+
try { result = task.run(task.abort.signal); } catch (error) { finish(task, error); return; }
|
|
43
|
+
Promise.resolve(result).then(value => finish(task, null, value), error => finish(task, error));
|
|
44
|
+
}
|
|
45
|
+
function pump() {
|
|
46
|
+
scheduled = false;
|
|
47
|
+
if (disposed) return;
|
|
48
|
+
const queued = [...tasks].filter(task => !task.running).sort((a, b) => b.priority - a.priority || a.order - b.order);
|
|
49
|
+
for (const task of queued) {
|
|
50
|
+
if (!tasks.has(task)) continue;
|
|
51
|
+
if (task.priority === 2) { start(task); continue; }
|
|
52
|
+
if (navigating) break;
|
|
53
|
+
const slots = policy(task.priority);
|
|
54
|
+
if (!slots) { task.abort.abort(abortError()); continue; }
|
|
55
|
+
if (active >= slots) continue;
|
|
56
|
+
start(task);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function wake() {
|
|
60
|
+
if (scheduled || disposed) return;
|
|
61
|
+
scheduled = true; queueMicrotask(pump);
|
|
62
|
+
}
|
|
63
|
+
function changed() {
|
|
64
|
+
for (const task of tasks) if (task.priority !== 2 && !policy(task.priority)) task.abort.abort(abortError());
|
|
65
|
+
wake();
|
|
66
|
+
}
|
|
67
|
+
doc?.addEventListener?.('visibilitychange', changed);
|
|
68
|
+
win?.addEventListener?.('online', changed);
|
|
69
|
+
win?.addEventListener?.('offline', changed);
|
|
70
|
+
connection?.addEventListener?.('change', changed);
|
|
71
|
+
return {
|
|
72
|
+
schedule(run, { priority, abort = new AbortController() } = {}) {
|
|
73
|
+
const rank = priorityOf(priority);
|
|
74
|
+
if (disposed || abort.signal.aborted || !policy(rank)) return undefined;
|
|
75
|
+
if (tasks.size >= limit) {
|
|
76
|
+
// Keep intent ahead of broad viewport scans. Never evict demand work.
|
|
77
|
+
const oldest = [...tasks].filter(task => task.priority < 2).sort((a, b) => a.priority - b.priority || a.order - b.order)[0];
|
|
78
|
+
if (!oldest || oldest.priority > rank) return undefined;
|
|
79
|
+
oldest.abort.abort(abortError());
|
|
80
|
+
}
|
|
81
|
+
const task = { run, abort, priority: rank, order: serial++ };
|
|
82
|
+
const promise = new Promise((resolve, reject) => { task.resolve = resolve; task.reject = reject; });
|
|
83
|
+
// Cancellation also settles a task whose producer ignores AbortSignal.
|
|
84
|
+
// A late result cannot regain a queue slot or resurrect an invalid cache.
|
|
85
|
+
task.cancelled = () => finish(task, abort.signal.reason || abortError());
|
|
86
|
+
tasks.add(task); abort.signal.addEventListener('abort', task.cancelled, { once: true });
|
|
87
|
+
wake();
|
|
88
|
+
return { abort, promise,
|
|
89
|
+
promote(priority = 'intent') { if (tasks.has(task)) { task.priority = Math.max(task.priority, priorityOf(priority)); wake(); } },
|
|
90
|
+
demand() { if (tasks.has(task)) { task.priority = 2; wake(); } },
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
navigation(target) {
|
|
94
|
+
navigating++;
|
|
95
|
+
target?.demand();
|
|
96
|
+
for (const task of tasks) if (task.abort !== target?.abort && task.priority !== 2) task.abort.abort(abortError());
|
|
97
|
+
let released = false;
|
|
98
|
+
return () => { if (!released) { released = true; navigating--; wake(); } };
|
|
99
|
+
},
|
|
100
|
+
dispose() {
|
|
101
|
+
if (disposed) return;
|
|
102
|
+
disposed = true;
|
|
103
|
+
doc?.removeEventListener?.('visibilitychange', changed);
|
|
104
|
+
win?.removeEventListener?.('online', changed);
|
|
105
|
+
win?.removeEventListener?.('offline', changed);
|
|
106
|
+
connection?.removeEventListener?.('change', changed);
|
|
107
|
+
for (const task of tasks) task.abort.abort(abortError());
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {pathToFileURL} from 'node:url';
|
|
3
|
+
// This is an import budget, not a total RSS limit. An individual module and its
|
|
4
|
+
// transitive imports can exceed it; imported JavaScript cannot be unloaded.
|
|
5
|
+
export async function preloadModules(files,{maxRssMb,timeoutMs},load=file=>import(pathToFileURL(file).href)){
|
|
6
|
+
const deadline=performance.now()+timeoutMs,loaded=[];
|
|
7
|
+
for(const file of files){
|
|
8
|
+
if(performance.now()>=deadline||process.memoryUsage().rss>=maxRssMb*1024*1024)break;
|
|
9
|
+
await load(path.resolve(file));loaded.push(file);
|
|
10
|
+
}
|
|
11
|
+
return loaded.length;
|
|
12
|
+
}
|
package/runtime/profile.mjs
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import presets from './profiles.json' with { type: 'json' };
|
|
2
2
|
|
|
3
|
+
export function rscResourceLimits(environment=process.env,production=environment.NODE_ENV!=='development'){
|
|
4
|
+
if(!production||/--max[-_]semi[-_]space[-_]size(?:[=\s]|$)/.test(environment.NODE_OPTIONS||''))return undefined;
|
|
5
|
+
return {maxYoungGenerationSizeMb:runtimeProfile(environment,production).youngGenerationMiB};
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
export function runtimeProfile(environment = process.env, production = environment.NODE_ENV !== 'development') {
|
|
4
9
|
const requested = environment.PRNEXT_PROFILE || (environment.PRNEXT_MEMORY_PROFILE === 'compact' ? 'compact' : 'balanced');
|
|
5
10
|
const name = requested === 'standard' ? 'classic' : requested;
|
|
6
11
|
if (!Object.hasOwn(presets, name)) throw new Error(`Unknown runtime profile ${JSON.stringify(name)}; use ${Object.keys(presets).join(', ')}`);
|
|
7
12
|
const effective = production ? name : 'classic';
|
|
13
|
+
if(production&&environment.PRNEXT_EFFECTIVE_PROFILE){
|
|
14
|
+
let value;
|
|
15
|
+
try{value=JSON.parse(environment.PRNEXT_EFFECTIVE_PROFILE);}catch{throw new Error('Invalid effective runtime profile');}
|
|
16
|
+
const settings=value?.[1],original=presets[effective];
|
|
17
|
+
if(!Array.isArray(value)||value.length!==2||value[0]!==effective||!settings||
|
|
18
|
+
Object.keys(settings).length!==Object.keys(original).length||Object.keys(original).some(key=>!Object.hasOwn(settings,key)||
|
|
19
|
+
(original[key]===null?settings[key]!==null&&(!Number.isSafeInteger(settings[key])||settings[key]<1):typeof original[key]==='boolean'?typeof settings[key]!=='boolean':!Number.isSafeInteger(settings[key])||settings[key]<1||settings[key]>Math.max(512,original[key]))))throw new Error('Invalid effective runtime profile');
|
|
20
|
+
return {name:effective,...settings};
|
|
21
|
+
}
|
|
8
22
|
return { name: effective, ...presets[effective] };
|
|
9
23
|
}
|
package/runtime/profiles.json
CHANGED
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
"renderIdleSeconds": null
|
|
41
41
|
},
|
|
42
42
|
"memory": {
|
|
43
|
-
"optimizeForSize":
|
|
43
|
+
"optimizeForSize": false,
|
|
44
44
|
"semiSpaceMiB": 2,
|
|
45
|
-
"youngGenerationMiB":
|
|
45
|
+
"youngGenerationMiB": 8,
|
|
46
46
|
"renderStarts": 8,
|
|
47
47
|
"liveResponses": 28,
|
|
48
48
|
"apiConcurrency": 64,
|
package/runtime/render.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {reportRequestError} from './instrumentation.mjs';
|
|
1
2
|
import { localePath } from '../compat/locale.cjs';
|
|
2
3
|
import React from 'react';
|
|
3
4
|
import DefaultApp, { loadGetInitialProps } from '../compat/app.cjs';
|
|
@@ -94,6 +95,7 @@ export async function renderPageRequest(options) {
|
|
|
94
95
|
try { result = await renderPage({ ...options, capturedResponse }); }
|
|
95
96
|
catch (error) {
|
|
96
97
|
if ([400, 504].includes(error?.statusCode) || options.signal?.aborted) throw error;
|
|
98
|
+
reportRequestError(error);
|
|
97
99
|
console.error('[prnext]', error?.stack || error);
|
|
98
100
|
return renderPageError(options, { statusCode: 500, error, responseHeaders: capturedResponse.getHeaders() });
|
|
99
101
|
}
|
|
@@ -266,6 +268,7 @@ export async function renderIsrPage(options) {
|
|
|
266
268
|
catch (error) {
|
|
267
269
|
if (!options.capturePageFailure) throw error;
|
|
268
270
|
const pageFailure = summarizePageFailure(error);
|
|
271
|
+
reportRequestError(error);
|
|
269
272
|
console.error('[prnext] Page generation failed:', pageFailure.stack || pageFailure.message);
|
|
270
273
|
return { status: 500, headers: {}, body: Buffer.alloc(0), pageFailure };
|
|
271
274
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Shared browser route matching; importing this never loads either router. */
|
|
2
|
+
export function matchPagePattern(pattern, pathname) {
|
|
3
|
+
let segments;
|
|
4
|
+
try { segments = pathname.replace(/\/+$/, '').split('/').slice(1).map(decodeURIComponent); }
|
|
5
|
+
catch { return null; }
|
|
6
|
+
if (segments.length === 1 && !segments[0]) segments = [];
|
|
7
|
+
if (segments.some(segment => !segment || /[/\\\0]/.test(segment) || segment === '.' || segment === '..')) return null;
|
|
8
|
+
const parts = pattern === '/' ? [] : pattern.split('/').slice(1), params = Object.create(null), rank = [];
|
|
9
|
+
let index = 0;
|
|
10
|
+
for (const part of parts) {
|
|
11
|
+
const optional = /^\[\[\.\.\.([^\]]+)\]\]$/.exec(part);
|
|
12
|
+
const catchall = /^\[\.\.\.([^\]]+)\]$/.exec(part);
|
|
13
|
+
const parameter = /^\[([^\]]+)\]$/.exec(part);
|
|
14
|
+
if (optional || catchall) {
|
|
15
|
+
if (catchall && index === segments.length) return null;
|
|
16
|
+
if (index !== segments.length) params[(optional || catchall)[1]] = segments.slice(index);
|
|
17
|
+
index = segments.length; rank.push(optional ? 0 : 1);
|
|
18
|
+
} else if (parameter) {
|
|
19
|
+
if (index === segments.length) return null;
|
|
20
|
+
params[parameter[1]] = segments[index++]; rank.push(2);
|
|
21
|
+
} else {
|
|
22
|
+
if (segments[index++] !== part) return null;
|
|
23
|
+
rank.push(3);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return index === segments.length ? { params, rank } : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isHtmlDocumentRoute(routes, pathname) {
|
|
30
|
+
let winner;
|
|
31
|
+
for (const route of routes || []) {
|
|
32
|
+
const match = matchPagePattern(route.pattern, pathname);
|
|
33
|
+
if (!match) continue;
|
|
34
|
+
const rank = winner && (() => {
|
|
35
|
+
for (let index = 0; index < Math.max(match.rank.length, winner.rank.length); index++) {
|
|
36
|
+
const difference = (match.rank[index] ?? 4) - (winner.rank[index] ?? 4);
|
|
37
|
+
if (difference) return difference;
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
})();
|
|
41
|
+
if (!winner || rank > 0) winner = { ...match, route };
|
|
42
|
+
}
|
|
43
|
+
return winner?.route.htmlOnly === true;
|
|
44
|
+
}
|