@thomas.f/prnext 0.1.12-alpha → 0.1.15-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/README.md +305 -65
- package/build/ADAPTERS.md +121 -0
- package/build/actions.mjs +4 -0
- package/build/app-config.mjs +2 -0
- package/build/app.mjs +23 -5
- package/build/build-lock.mjs +71 -0
- package/build/bundle-runtime.mjs +82 -0
- package/build/compiler.mjs +3 -1
- package/build/config.mjs +9 -2
- package/build/font-cache.mjs +50 -0
- package/build/font-files.mjs +5 -3
- package/build/fonts.mjs +1 -1
- package/build/index.mjs +12 -1
- package/build/inspect.mjs +20 -2
- package/build/inspection-summary.mjs +37 -0
- package/build/metadata-conflicts.mjs +13 -0
- package/build/metadata-images.mjs +3 -3
- package/build/metadata-routes.mjs +3 -3
- package/build/native-packages.mjs +5 -1
- package/build/npm-packages.mjs +2 -1
- package/build/og.mjs +3 -9
- package/build/optimize-imports.mjs +112 -0
- package/build/output-contract.mjs +41 -0
- package/build/precompress.mjs +13 -0
- package/build/proxy-body-config.mjs +10 -0
- package/build/scan.mjs +10 -8
- package/build/static-adapter.mjs +100 -0
- package/cli.mjs +64 -11
- package/compat/after.cjs +51 -0
- package/compat/diagnostics.cjs +4 -0
- package/compat/index.d.ts +32 -1
- package/compat/server.cjs +2 -1
- package/compat/server.d.cts +2 -0
- package/index.mjs +2 -0
- package/native/launch.mjs +19 -0
- package/package.json +8 -9
- package/release/verify.mjs +2 -0
- package/runtime/api.mjs +20 -7
- package/runtime/app-render.mjs +78 -33
- package/runtime/app-route-plan.mjs +45 -0
- package/runtime/app-routing-state.mjs +101 -21
- package/runtime/app-routing.mjs +37 -18
- package/runtime/app-segments.mjs +25 -11
- package/runtime/app-static.mjs +1 -1
- package/runtime/app-stream-html.mjs +2 -1
- package/runtime/cache-request.mjs +10 -2
- package/runtime/cloud/archive.mjs +317 -0
- package/runtime/cloud/client.mjs +641 -0
- package/runtime/cloud/environment.mjs +60 -0
- package/runtime/cloud/state.mjs +156 -0
- package/runtime/cloud/upload.mjs +48 -0
- package/runtime/html-envelope.mjs +70 -7
- package/runtime/http.mjs +7 -4
- package/runtime/middleware.mjs +3 -3
- package/runtime/og/Geist-Regular.ttf +0 -0
- package/runtime/og/LICENSE +373 -0
- package/runtime/og/LICENSE.Geist +93 -0
- package/runtime/og/LICENSE.fflate +21 -0
- package/runtime/og/LICENSE.harfbuzzjs +22 -0
- package/runtime/og/LICENSE.satori +373 -0
- package/runtime/og/NOTICE.txt +4 -0
- package/runtime/og/hb.wasm +0 -0
- package/runtime/og/index.node.mjs +31260 -0
- package/runtime/og/resvg.wasm +0 -0
- package/runtime/og-backend.mjs +8 -0
- package/runtime/og-loader.mjs +2 -15
- package/runtime/request-transport.mjs +1 -1
- package/runtime/route-static.mjs +1 -1
- package/runtime/rsc-worker.mjs +27 -4
- package/runtime/stream-utils.mjs +22 -3
- package/runtime/transport.mjs +16 -0
- package/runtime/worker-metadata.mjs +23 -0
- package/runtime/worker.mjs +36 -9
package/compat/index.d.ts
CHANGED
|
@@ -118,6 +118,8 @@ export interface NextConfig {
|
|
|
118
118
|
workerRetirement?: { idleSeconds?: number; laneIdleSeconds?: number; keepWarm?: boolean };
|
|
119
119
|
/** Next dev page-retention hints; PRNext builds all routes without that eviction queue. */
|
|
120
120
|
onDemandEntries?: { maxInactiveAge?: number; pagesBufferLength?: number };
|
|
121
|
+
/** Disable the Next development indicator. Error overlays and Fast Refresh remain enabled. */
|
|
122
|
+
devIndicators?: false;
|
|
121
123
|
i18n?: { locales: string[]; defaultLocale: string; localeDetection?: boolean; domains?: Array<{domain: string; defaultLocale: string; locales?: string[]; http?: boolean}> };
|
|
122
124
|
webpack?: (config: import('webpack').Configuration, context: { buildId: string; dev: boolean; isServer: boolean; nextRuntime?: 'nodejs' | 'edge'; webpack: typeof import('webpack'); defaultLoaders: {babel: {loader: string}} }) => import('webpack').Configuration | Promise<import('webpack').Configuration>;
|
|
123
125
|
turbopack?: { root?: string; rules?: Record<string, TurbopackRule | TurbopackRule[]>; resolveAlias?: Record<string, string | { browser: string }>; resolveExtensions?: string[] };
|
|
@@ -141,7 +143,7 @@ export interface NextConfig {
|
|
|
141
143
|
implementation?: 'sass' | 'sass-embedded';
|
|
142
144
|
[option: string]: unknown;
|
|
143
145
|
};
|
|
144
|
-
experimental?: { nextScriptWorkers?: boolean; serverActions?: true | { allowedOrigins?: string[]; bodySizeLimit?: number | string } };
|
|
146
|
+
experimental?: { nextScriptWorkers?: boolean; optimizePackageImports?: string[]; proxyClientMaxBodySize?: number | string; serverActions?: true | { allowedOrigins?: string[]; bodySizeLimit?: number | string } };
|
|
145
147
|
trailingSlash?: boolean;
|
|
146
148
|
skipTrailingSlashRedirect?: boolean;
|
|
147
149
|
skipMiddlewareUrlNormalize?: boolean;
|
|
@@ -166,4 +168,33 @@ export interface NextConfig {
|
|
|
166
168
|
rewrites?: () => Rewrite[] | { beforeFiles?: Rewrite[]; afterFiles?: Rewrite[]; fallback?: Rewrite[] } | Promise<Rewrite[] | { beforeFiles?: Rewrite[]; afterFiles?: Rewrite[]; fallback?: Rewrite[] }>;
|
|
167
169
|
}
|
|
168
170
|
export type PRNextConfig = NextConfig;
|
|
171
|
+
/** Public build envelope; internal manifest fields are adapter-specific. */
|
|
172
|
+
export type BuildOutput = {
|
|
173
|
+
version: 1;
|
|
174
|
+
buildId: string;
|
|
175
|
+
manifest: 'manifest.json';
|
|
176
|
+
} & ({
|
|
177
|
+
adapter: 'react';
|
|
178
|
+
assets: 'assets';
|
|
179
|
+
server: { entry: 'runtime/worker.mjs'; runtimeContract: 'runtime-contract.json' };
|
|
180
|
+
fallback: null;
|
|
181
|
+
} | {
|
|
182
|
+
adapter: 'static';
|
|
183
|
+
assets: 'static';
|
|
184
|
+
server: null;
|
|
185
|
+
fallback: null | 'index.html';
|
|
186
|
+
});
|
|
187
|
+
export interface StaticBuildOptions {
|
|
188
|
+
from?: string;
|
|
189
|
+
spa?: boolean;
|
|
190
|
+
distDir?: string;
|
|
191
|
+
dev?: false;
|
|
192
|
+
}
|
|
193
|
+
export interface BuildResult { buildId: string; outputDirectory: string; }
|
|
194
|
+
export interface StaticBuildResult extends BuildResult { files: number; bytes: number; }
|
|
195
|
+
export function build(projectRoot: string, options: StaticBuildOptions & { adapter: 'static' }): Promise<StaticBuildResult>;
|
|
196
|
+
export function build(projectRoot: string, options?: { adapter?: 'react'; dev?: boolean; envMode?: 'production' | 'development' | 'test' }): Promise<BuildResult>;
|
|
197
|
+
export function buildStatic(projectRoot: string, options?: StaticBuildOptions): Promise<StaticBuildResult>;
|
|
198
|
+
export function readBuildOutput(buildDirectory: string): Promise<BuildOutput>;
|
|
199
|
+
export function validateBuildOutput(value: unknown): BuildOutput;
|
|
169
200
|
export type { DocumentContext, DocumentInitialProps, DocumentProps } from './document.cjs';
|
package/compat/server.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const { NextRequest, NextResponse, NextURL } = require('./server-primitives.cjs');
|
|
3
|
+
const { after } = require('./after.cjs');
|
|
3
4
|
|
|
4
5
|
async function connection() {
|
|
5
6
|
const context = require('./headers.cjs').currentRequest();
|
|
@@ -10,4 +11,4 @@ async function connection() {
|
|
|
10
11
|
dynamicUsage('connection()', context);
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
module.exports = { NextRequest, NextResponse, NextURL, connection };
|
|
14
|
+
module.exports = { NextRequest, NextResponse, NextURL, connection, after };
|
package/compat/server.d.cts
CHANGED
|
@@ -35,3 +35,5 @@ export interface MiddlewareConfig {
|
|
|
35
35
|
export interface ProxyConfig { matcher?: MiddlewareConfig['matcher'] }
|
|
36
36
|
/** Exclude the following render from prerendering until a request is available. */
|
|
37
37
|
export function connection(): Promise<void>;
|
|
38
|
+
/** Post-response work in Node Route Handlers. Other after() lifecycles fail explicitly. */
|
|
39
|
+
export function after(task: Promise<unknown> | (() => unknown)): void;
|
package/index.mjs
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// A production CLI has nothing to supervise once the native server starts.
|
|
2
|
+
// On supported Linux Node releases, replace it instead of retaining a Node heap.
|
|
3
|
+
// Keep the portable parent/pipe lifecycle for Windows, older Node, IPC callers
|
|
4
|
+
// and instrumentation which may own descriptors or need JS exit handlers.
|
|
5
|
+
export function replaceWithNative(binary, args, env, runtime = process) {
|
|
6
|
+
const options = [...(runtime.execArgv || []), env.NODE_OPTIONS || ''].join(' ');
|
|
7
|
+
// Only heap sizing is known not to require the Node launcher's lifetime.
|
|
8
|
+
// Be conservative for all other Node flags, including future diagnostics,
|
|
9
|
+
// loaders, preloads, permissions and signal-triggered reports/snapshots.
|
|
10
|
+
const customRuntime = options.replace(/(?:^|\s)--max[-_](?:semi|old)[-_]space[-_]size(?:=|\s+)\d+(?=\s|$)/g, '').trim();
|
|
11
|
+
if (runtime.platform !== 'linux' || typeof runtime.execve !== 'function'
|
|
12
|
+
|| typeof runtime.send === 'function' || env.NODE_V8_COVERAGE || customRuntime) return false;
|
|
13
|
+
// execve keeps stdin/out/err and the PID. No parent pipe is needed: signals
|
|
14
|
+
// reach Rust directly, and its worker pipes still supervise every Node child.
|
|
15
|
+
// The marker also makes an old native binary fail clearly instead of using
|
|
16
|
+
// this lifecycle without the Linux parent-death guard it requires.
|
|
17
|
+
runtime.execve(binary, [binary, ...args, '--cli-exec'], env);
|
|
18
|
+
return true; // Only reached by injected test runtimes; real execve never returns.
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thomas.f/prnext",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15-alpha",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Experimental Rust-powered runtime for Next.js apps, React and npm",
|
|
6
6
|
"homepage": "https://prnext.dev",
|
|
@@ -111,7 +111,6 @@
|
|
|
111
111
|
"@babel/types": "^7.29.0",
|
|
112
112
|
"@edge-runtime/vm": "5.0.0",
|
|
113
113
|
"@vercel/nft": "1.11.0",
|
|
114
|
-
"@vercel/og": "1.0.3",
|
|
115
114
|
"dotenv": "16.6.1",
|
|
116
115
|
"dotenv-expand": "11.0.7",
|
|
117
116
|
"esbuild": "^0.28.2",
|
|
@@ -133,14 +132,14 @@
|
|
|
133
132
|
"react-server-dom-webpack": "19.3.0"
|
|
134
133
|
},
|
|
135
134
|
"optionalDependencies": {
|
|
136
|
-
"prnext-darwin-arm64": "0.1.
|
|
137
|
-
"prnext-darwin-x64": "0.1.
|
|
138
|
-
"prnext-linux-arm64-gnu": "0.1.
|
|
139
|
-
"prnext-linux-x64-gnu": "0.1.
|
|
140
|
-
"@thomas.f/prnext-win32-x64-msvc": "0.1.
|
|
141
|
-
"@thomas.f/prnext-win32-arm64-msvc": "0.1.
|
|
135
|
+
"prnext-darwin-arm64": "0.1.15-alpha",
|
|
136
|
+
"prnext-darwin-x64": "0.1.15-alpha",
|
|
137
|
+
"prnext-linux-arm64-gnu": "0.1.15-alpha",
|
|
138
|
+
"prnext-linux-x64-gnu": "0.1.15-alpha",
|
|
139
|
+
"@thomas.f/prnext-win32-x64-msvc": "0.1.15-alpha",
|
|
140
|
+
"@thomas.f/prnext-win32-arm64-msvc": "0.1.15-alpha"
|
|
142
141
|
},
|
|
143
142
|
"engines": {
|
|
144
|
-
"node": ">=
|
|
143
|
+
"node": "^22.23.4 || ^24.22.1 || >=26.12.1"
|
|
145
144
|
}
|
|
146
145
|
}
|
package/release/verify.mjs
CHANGED
|
@@ -22,6 +22,8 @@ export async function verifyPackage() {
|
|
|
22
22
|
}
|
|
23
23
|
await exported(pkg.exports);
|
|
24
24
|
await access(path.join(packageRoot, 'runtime/profiles.json'));
|
|
25
|
+
for (const name of ['index.node.mjs', 'resvg.wasm', 'hb.wasm', 'Geist-Regular.ttf', 'LICENSE', 'LICENSE.fflate', 'LICENSE.Geist', 'LICENSE.harfbuzzjs', 'LICENSE.satori', 'NOTICE.txt']) await access(path.join(packageRoot, 'runtime/og', name));
|
|
26
|
+
if (pkg.dependencies['@vercel/og'] || (await readFile(path.join(packageRoot, 'runtime/og/index.node.mjs'), 'utf8')).includes('fflate@0.7.3')) throw new Error('OG renderer still depends on vulnerable upstream fflate. Run scripts/prepare-og.mjs.');
|
|
25
27
|
const source = sourceCheckout();
|
|
26
28
|
if (source) {
|
|
27
29
|
const cargo = await readFile(path.join(source, 'crates/prnext/Cargo.toml'), 'utf8');
|
package/runtime/api.mjs
CHANGED
|
@@ -131,7 +131,7 @@ async function runStreamingPagesApi(handler, request, { method, signal, timeoutM
|
|
|
131
131
|
const timer = setTimeout(() => cancelRequest(apiTimeoutError('response headers', timeoutMs)), timeoutMs);
|
|
132
132
|
timer.unref();
|
|
133
133
|
if (controller.signal.aborted) cancelRequest(abortError(controller.signal.reason));
|
|
134
|
-
else trackRequestWork(Promise.resolve().then(() => handler(request, response)), signal).then(result => {
|
|
134
|
+
else trackRequestWork(Promise.resolve().then(() => handler(request, response)), signal, { timeoutMs }).then(result => {
|
|
135
135
|
if (result !== undefined && result !== response && !response.writableEnded) {
|
|
136
136
|
throw new Error('Pages API handlers must send their response with res.send(), res.json(), or res.end()');
|
|
137
137
|
}
|
|
@@ -178,6 +178,7 @@ function parseBody(bytes, contentType = '') {
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
export function runApi(options) {
|
|
181
|
+
if (options.timeoutMs === undefined && options.route?.cacheConfig?.maxDuration) options = { ...options, timeoutMs: options.route.cacheConfig.maxDuration * 1000 };
|
|
181
182
|
return withCacheRequest(options, options.route?.router === 'app' ? 'route' : 'pages', context => runApiInner(options, context));
|
|
182
183
|
}
|
|
183
184
|
|
|
@@ -189,11 +190,19 @@ async function runApiInner({ modulePath, route = {}, manifest = {}, production =
|
|
|
189
190
|
const mod = (pages && loadedPagesModule(modulePath)) || await withSignal(loadModule(modulePath, pages), stream ? signal : undefined);
|
|
190
191
|
const requestOriginal = originalUrl ? new URL(originalUrl, url) : undefined;
|
|
191
192
|
if (requestOriginal) requestOriginal.pathname = removeBasePath(requestOriginal.pathname, manifest.config?.basePath || '');
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
const appHandler = typeof mod.default !== 'function';
|
|
194
|
+
// App Route Handlers receive a NextRequest below. Constructing the Pages
|
|
195
|
+
// IncomingMessage here duplicates URL, header and cookie work and allocates
|
|
196
|
+
// an unused Duplex. Keep the existing Pages request path unchanged.
|
|
197
|
+
const { request, bytes } = appHandler
|
|
198
|
+
? { bytes: Buffer.isBuffer(body) ? body : Buffer.from(body || '', 'base64') }
|
|
199
|
+
: createRequest({ url, originalUrl: requestOriginal?.href, params, method, headers, body });
|
|
200
|
+
if (!appHandler) {
|
|
201
|
+
request.draftMode = cacheContext.draftMode;
|
|
202
|
+
request.previewData = cacheContext.previewData;
|
|
203
|
+
request.preview = cacheContext.previewData !== false;
|
|
204
|
+
}
|
|
205
|
+
if (appHandler) {
|
|
197
206
|
const {NextRequest, proxyRouteRequest} = await appRequestPrimitives();
|
|
198
207
|
if (route.router === 'app' && manifest.dev) {
|
|
199
208
|
const listing = await collectStaticParams({ page: mod, pageConfig: route.handlerConfig, segments: [] }, route.pattern);
|
|
@@ -221,6 +230,7 @@ async function runApiInner({ modulePath, route = {}, manifest = {}, production =
|
|
|
221
230
|
const timeout = lifecycle && setTimeout(() => lifecycle.controller.abort(apiTimeoutError('response headers', timeoutMs)), timeoutMs);
|
|
222
231
|
timeout?.unref();
|
|
223
232
|
const pendingResponse = runRequestContext({ url, method, headers, params, production, cacheComponents: manifest.config?.cacheComponents, cacheLife: manifest.config?.cacheLife, cacheHandlers: manifest.config?.cacheHandlers, cacheHandler: manifest.config?.cacheHandler, cacheMaxMemorySize: manifest.config?.cacheMaxMemorySize, distDir: path.dirname(path.dirname(modulePath)), signal: lifecycle?.controller.signal || signal, previewModeId: manifest.previewModeId, basePath: manifest.config?.basePath || '', routePattern: route.pattern, cacheConfig: route.cacheConfig, phase: 'route', cacheState: cacheContext.cacheState, staticState: cacheContext.staticState, mutableCookies: true }, async () => {
|
|
233
|
+
currentRequest().afterScope = cacheContext.afterScope;
|
|
224
234
|
let result;
|
|
225
235
|
try { result = await handler(proxyRouteRequest(new NextRequest(requestOriginal || url, init), currentRequest()), { params: Promise.resolve(params) }); }
|
|
226
236
|
catch (error) {
|
|
@@ -245,7 +255,10 @@ async function runApiInner({ modulePath, route = {}, manifest = {}, production =
|
|
|
245
255
|
if (lifecycle) pendingResponse.then(({ result }) => {
|
|
246
256
|
if (lifecycle.controller.signal.aborted) result?.body?.cancel(lifecycle.controller.signal.reason).catch(() => {});
|
|
247
257
|
}, () => {});
|
|
248
|
-
|
|
258
|
+
// A client disconnect does not cancel arbitrary npm/database promises.
|
|
259
|
+
// Let them settle within this route's existing deadline before retiring the
|
|
260
|
+
// shared worker; the shorter orphan guard would interrupt other visitors.
|
|
261
|
+
trackRequestWork(pendingResponse, signal, { timeoutMs });
|
|
249
262
|
let result, changedCookies, runInContext;
|
|
250
263
|
try {
|
|
251
264
|
({ result, changedCookies, runInContext } = await withSignal(pendingResponse, lifecycle?.controller.signal));
|
package/runtime/app-render.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { addBasePath, removeBasePath } from '../compat/paths.cjs';
|
|
|
7
7
|
import { isDynamicBailout } from '../compat/dynamic-bailout.cjs';
|
|
8
8
|
import { Readable } from 'node:stream';
|
|
9
9
|
import { Worker } from 'node:worker_threads';
|
|
10
|
-
import { workerMetadata } from './worker-metadata.mjs';
|
|
10
|
+
import { workerMetadata, freezeClientModules, isImmutableClientModules } from './worker-metadata.mjs';
|
|
11
11
|
import { workerArtifacts } from './worker-artifacts.mjs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { PARTIAL_PREFETCH_TYPE, PARTIAL_PREFETCH_BYTES } from './app-prefetch.mjs';
|
|
@@ -40,6 +40,7 @@ const edgeManifests = new WeakMap();
|
|
|
40
40
|
const emptyManifest = Object.freeze({});
|
|
41
41
|
let nextId = 0;
|
|
42
42
|
const pending = new Map();
|
|
43
|
+
const noop = () => {};
|
|
43
44
|
// Avoid temporary arrays on every completion/cancellation at high concurrency.
|
|
44
45
|
function hasPending(worker, minimum = 1, cancelledOnly = false) {
|
|
45
46
|
let count = 0;
|
|
@@ -72,14 +73,14 @@ function flightDecoder(production) {
|
|
|
72
73
|
// React's development decoder guards module initialization with NODE_ENV.
|
|
73
74
|
// This require is synchronous; restore the host before any stream work.
|
|
74
75
|
process.env.NODE_ENV = production ? 'production' : 'development';
|
|
75
|
-
let decoder = require(filename)
|
|
76
|
-
if (typeof decoder !== 'function') {
|
|
76
|
+
let decoder = require(filename);
|
|
77
|
+
if (typeof decoder.createFromNodeStream !== 'function') {
|
|
77
78
|
// A consumer may have previously required the guarded development file
|
|
78
79
|
// in production, leaving an empty module in Node's cache.
|
|
79
80
|
delete require.cache[filename];
|
|
80
|
-
decoder = require(filename)
|
|
81
|
+
decoder = require(filename);
|
|
81
82
|
}
|
|
82
|
-
if (typeof decoder !== 'function') throw new Error('React Flight decoder is unavailable for the requested render mode');
|
|
83
|
+
if (typeof decoder.createFromNodeStream !== 'function') throw new Error('React Flight decoder is unavailable for the requested render mode');
|
|
83
84
|
flightDecoders.set(production, decoder);
|
|
84
85
|
return decoder;
|
|
85
86
|
} finally {
|
|
@@ -182,7 +183,7 @@ function getWorker(production) {
|
|
|
182
183
|
}
|
|
183
184
|
if (type === 'chunk') {
|
|
184
185
|
try { request.channel.chunk(chunk, byteOffset, byteLength); }
|
|
185
|
-
catch (error) { request.channel.error(error); current.postMessage({ id, type: 'cancel' }); request.cancel(error); }
|
|
186
|
+
catch (error) { request.channel.error(error); current.postMessage({ id, type: 'cancel' }); request.cancel(error); return; }
|
|
186
187
|
if (!done) return;
|
|
187
188
|
}
|
|
188
189
|
if (request.completed) {
|
|
@@ -194,6 +195,8 @@ function getWorker(production) {
|
|
|
194
195
|
// work can legitimately continue afterwards and retains its own bounds;
|
|
195
196
|
// supervise isolate responsiveness, not total background-task duration.
|
|
196
197
|
clearTimeout(request.timeout);
|
|
198
|
+
// A destroyed Timeout can still own its AsyncLocalStorage frame.
|
|
199
|
+
request.timeout = undefined;
|
|
197
200
|
request.releaseWorkWatch = workWatchdog.add();
|
|
198
201
|
request.detach?.();
|
|
199
202
|
if (type === 'end' || done) request.channel.end();
|
|
@@ -219,7 +222,11 @@ function getWorker(production) {
|
|
|
219
222
|
// Keep only settlement bookkeeping after sending the body. The
|
|
220
223
|
// isolate can still be flushing cache writes after its terminal frame.
|
|
221
224
|
request.channel = undefined;
|
|
222
|
-
|
|
225
|
+
// Both closures capture the original response promise / AbortSignal.
|
|
226
|
+
// Detaching the listener alone does not release those references while
|
|
227
|
+
// legitimate background work keeps this entry in the pending map.
|
|
228
|
+
request.cancel = request.detach = undefined;
|
|
229
|
+
request.resolve = request.reject = noop;
|
|
223
230
|
});
|
|
224
231
|
current.on('error', fail);
|
|
225
232
|
current.on('exit', code => fail(new Error(`App Router worker exited (${code})`), true));
|
|
@@ -227,12 +234,41 @@ function getWorker(production) {
|
|
|
227
234
|
return current;
|
|
228
235
|
}
|
|
229
236
|
|
|
237
|
+
// Stdio workers can retain referenced MessagePorts after rendering (including
|
|
238
|
+
// diagnostics piped from the RSC isolate). EOF must drain real cache/after()
|
|
239
|
+
// settlement before explicitly closing those isolates, not wait for Node's
|
|
240
|
+
// incidental event-loop liveness or terminate unfinished application work.
|
|
241
|
+
export async function drainAppRuntime() {
|
|
242
|
+
while (pending.size) await Promise.all([...pending.values()].map(request => request.settled));
|
|
243
|
+
await closeAppRuntime();
|
|
244
|
+
}
|
|
245
|
+
|
|
230
246
|
export function closeAppRuntime() {
|
|
231
247
|
const current = [...workers.values()];
|
|
232
248
|
return Promise.all(current.map(worker => lifecycles.get(worker).terminate()));
|
|
233
249
|
}
|
|
234
250
|
|
|
235
|
-
|
|
251
|
+
function requestDeadline(entry, lifecycle, id, hardTimeoutMs, delayMs) {
|
|
252
|
+
// Keep this callback outside renderFlight's request closure: cancelled work
|
|
253
|
+
// retains its hard deadline, but must not thereby retain the HTTP signal or
|
|
254
|
+
// response promise until an unrelated cache write settles.
|
|
255
|
+
return setTimeout(() => {
|
|
256
|
+
// A blocking npm dependency cannot honor an AbortSignal. Retire the
|
|
257
|
+
// isolate at the hard deadline so subsequent requests can recover.
|
|
258
|
+
void lifecycle.terminate(appTimeoutError('RSC worker', hardTimeoutMs)).finally(entry.settle).catch(error => console.error('[prnext] RSC worker termination failed:', error));
|
|
259
|
+
pending.delete(id);
|
|
260
|
+
entry.lease.release();
|
|
261
|
+
const error = appTimeoutError('RSC worker', hardTimeoutMs);
|
|
262
|
+
if (entry.completed) console.error('[prnext] RSC background work failed:', error);
|
|
263
|
+
entry.channel?.error(error);
|
|
264
|
+
entry.detach?.();
|
|
265
|
+
entry.reject(error);
|
|
266
|
+
}, delayMs);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export async function renderFlight(request, { softTimeoutMs, hardTimeoutMs, signal, deferFinalizer } = {}) {
|
|
270
|
+
softTimeoutMs ??= request.cacheConfig?.maxDuration ? request.cacheConfig.maxDuration * 1000 : 25_000;
|
|
271
|
+
hardTimeoutMs ??= request.cacheConfig?.maxDuration ? request.cacheConfig.maxDuration * 1000 : 28_000;
|
|
236
272
|
const deadline = performance.now() + hardTimeoutMs;
|
|
237
273
|
let current, lifecycle, lease;
|
|
238
274
|
for (;;) {
|
|
@@ -254,20 +290,10 @@ export async function renderFlight(request, { softTimeoutMs = 25_000, hardTimeou
|
|
|
254
290
|
current.ref();
|
|
255
291
|
const id = ++nextId;
|
|
256
292
|
let settle;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
void lifecycle.terminate(appTimeoutError('RSC worker', hardTimeoutMs)).finally(settle).catch(error => console.error('[prnext] RSC worker termination failed:', error));
|
|
262
|
-
pending.delete(id);
|
|
263
|
-
lease.release();
|
|
264
|
-
const error = appTimeoutError('RSC worker', hardTimeoutMs);
|
|
265
|
-
if (entry.completed) console.error('[prnext] RSC background work failed:', error);
|
|
266
|
-
entry.channel?.error(error);
|
|
267
|
-
entry.detach?.();
|
|
268
|
-
reject(error);
|
|
269
|
-
}, Math.max(0, deadline - performance.now()));
|
|
270
|
-
const entry = { worker: current, resolve, reject, timeout, operation: request.operation, lease, settle };
|
|
293
|
+
const settled = new Promise(resolve => { settle = resolve; });
|
|
294
|
+
trackRequestWork(settled, signal, { deadline, supervised: true });
|
|
295
|
+
const entry = { worker: current, resolve, reject, operation: request.operation, lease, settle, settled };
|
|
296
|
+
const timeout = entry.timeout = requestDeadline(entry, lifecycle, id, hardTimeoutMs, Math.max(0, deadline - performance.now()));
|
|
271
297
|
function cancel(reason) {
|
|
272
298
|
if (entry.cancelled) return;
|
|
273
299
|
// Keep the independent deadline until the isolate confirms settlement.
|
|
@@ -278,7 +304,11 @@ export async function renderFlight(request, { softTimeoutMs = 25_000, hardTimeou
|
|
|
278
304
|
// Pause new work at 16 abandoned requests while their acknowledgements
|
|
279
305
|
// settle. Existing mutation/cache work keeps its original hard deadline.
|
|
280
306
|
if (!hasPending(current) && !lifecycle.retiring) current.unref();
|
|
281
|
-
reject(reason instanceof Error ? reason : new Error('App Router response was cancelled'));
|
|
307
|
+
entry.reject(reason instanceof Error ? reason : new Error('App Router response was cancelled'));
|
|
308
|
+
// Settlement/deadline tracking remains live, but a disconnected caller
|
|
309
|
+
// no longer needs its stream, promise or signal retained by this entry.
|
|
310
|
+
entry.channel = entry.cancel = entry.detach = undefined;
|
|
311
|
+
entry.resolve = entry.reject = noop;
|
|
282
312
|
}
|
|
283
313
|
entry.cancel = cancel;
|
|
284
314
|
if (request.stream) entry.channel = parentStreamChannel(current, id, cancel);
|
|
@@ -294,6 +324,10 @@ export async function renderFlight(request, { softTimeoutMs = 25_000, hardTimeou
|
|
|
294
324
|
pending.set(id, entry);
|
|
295
325
|
try {
|
|
296
326
|
const wire = { ...request, renderTimeoutMs: Math.min(softTimeoutMs, Math.max(1, deadline - performance.now())) };
|
|
327
|
+
if (request.action && deferFinalizer) {
|
|
328
|
+
wire.waitForResponse = true;
|
|
329
|
+
deferFinalizer(() => { if (pending.has(id)) current.postMessage({ id, type: 'after-response' }); });
|
|
330
|
+
}
|
|
297
331
|
if (request.production) {
|
|
298
332
|
const artifactId = artifacts.get(current)(wire.partialFlight);
|
|
299
333
|
if (artifactId !== undefined) { wire.partialFlightId = artifactId; delete wire.partialFlight; }
|
|
@@ -317,11 +351,12 @@ export async function renderFlight(request, { softTimeoutMs = 25_000, hardTimeou
|
|
|
317
351
|
}
|
|
318
352
|
|
|
319
353
|
export async function decodeFlight(body, clientModules = {}, distDir, { production = process.env.NODE_ENV === 'production' } = {}) {
|
|
320
|
-
const ids = Object.keys(clientModules);
|
|
321
354
|
let cached = ssrManifests.get(clientModules);
|
|
322
355
|
// Keep only the manifest's module mapping, never a decoded React model or
|
|
323
356
|
// request context. Weak ownership releases it with the build manifest.
|
|
324
|
-
|
|
357
|
+
const immutable = isImmutableClientModules(clientModules);
|
|
358
|
+
const ids = cached && cached.distDir === distDir && cached.immutable && immutable ? null : Object.keys(clientModules);
|
|
359
|
+
if (!cached || cached.distDir !== distDir || (ids && (ids.length !== cached.paths.size || ids.some(id => cached.paths.get(id) !== clientModules[id].ssrModule)))) {
|
|
325
360
|
const moduleMap = Object.create(null), paths = new Map();
|
|
326
361
|
for (const id of ids) {
|
|
327
362
|
const item = clientModules[id];
|
|
@@ -332,13 +367,20 @@ export async function decodeFlight(body, clientModules = {}, distDir, { producti
|
|
|
332
367
|
moduleMap[id] = { '*': { id: moduleUrl, chunks: [moduleUrl, moduleUrl], name: '*' } };
|
|
333
368
|
paths.set(id, item.ssrModule);
|
|
334
369
|
}
|
|
335
|
-
cached = { distDir, paths, moduleMap };
|
|
370
|
+
cached = { distDir, paths, moduleMap, immutable };
|
|
336
371
|
ssrManifests.set(clientModules, cached);
|
|
337
|
-
}
|
|
372
|
+
} else if (immutable) cached.immutable = true;
|
|
338
373
|
// Hosts can build/render both modes in one process. The encoded protocol,
|
|
339
374
|
// rather than the host's current environment or import cache, selects React.
|
|
340
375
|
const decoder = flightDecoder(Boolean(production));
|
|
341
|
-
|
|
376
|
+
const serverConsumerManifest = { moduleMap: cached.moduleMap, serverModuleMap: null, moduleLoading: null };
|
|
377
|
+
// React's Node decoder can consume Web streams directly. Keep the Flight
|
|
378
|
+
// bytes in their original stream instead of allocating a Node Readable,
|
|
379
|
+
// forwarding its events and buffering a second queue for each response.
|
|
380
|
+
if (body instanceof ReadableStream && typeof decoder.createFromReadableStream === 'function') {
|
|
381
|
+
return decoder.createFromReadableStream(body, { serverConsumerManifest });
|
|
382
|
+
}
|
|
383
|
+
return decoder.createFromNodeStream(body instanceof ReadableStream ? Readable.fromWeb(body) : Readable.from([body]), serverConsumerManifest);
|
|
342
384
|
}
|
|
343
385
|
|
|
344
386
|
export async function renderHtml(tree, router, { timeoutMs = 25_000, formState = null, staticGeneration = false, nonce, strictMode = true } = {}) {
|
|
@@ -414,13 +456,16 @@ export async function renderAppPage(options) {
|
|
|
414
456
|
return response;
|
|
415
457
|
}
|
|
416
458
|
|
|
417
|
-
async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distDir = path.dirname(path.dirname(modulePath)), url = 'http://localhost/', originalUrl, params = {}, method = 'GET', headers = {}, routingRequestHeaders, routingResolver, body = '', production = process.env.NODE_ENV === 'production', stream = false, signal }) {
|
|
459
|
+
async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distDir = path.dirname(path.dirname(modulePath)), url = 'http://localhost/', originalUrl, params = {}, method = 'GET', headers = {}, routingRequestHeaders, routingResolver, body = '', production = process.env.NODE_ENV === 'production', stream = false, signal, deferFinalizer }) {
|
|
418
460
|
let clientModules = manifest.app?.clientModules || emptyManifest;
|
|
419
461
|
if (route.cacheConfig?.runtime === 'edge') {
|
|
420
462
|
let edge = !manifest.dev && edgeManifests.get(clientModules);
|
|
421
463
|
if (!edge) {
|
|
422
464
|
edge = Object.fromEntries(Object.entries(clientModules).map(([id, item]) => [id, item.edgeSsrModule ? { ...item, ssrModule: item.edgeSsrModule } : item]));
|
|
423
|
-
if (!manifest.dev)
|
|
465
|
+
if (!manifest.dev) {
|
|
466
|
+
if (isImmutableClientModules(clientModules)) freezeClientModules(edge);
|
|
467
|
+
edgeManifests.set(clientModules, edge);
|
|
468
|
+
}
|
|
424
469
|
}
|
|
425
470
|
clientModules = edge;
|
|
426
471
|
}
|
|
@@ -490,7 +535,7 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
|
|
|
490
535
|
if (stream && (!request.action || request.action.id)) {
|
|
491
536
|
let progressive;
|
|
492
537
|
try {
|
|
493
|
-
progressive = await renderFlight({ ...request, stream: true }, { signal });
|
|
538
|
+
progressive = await renderFlight({ ...request, stream: true }, { signal, deferFinalizer });
|
|
494
539
|
const rsc = Object.entries(headers).some(([name, value]) => name.toLowerCase() === 'rsc' && value === '1');
|
|
495
540
|
if (rsc || request.action?.id) {
|
|
496
541
|
if (method === 'HEAD') await progressive.body.cancel();
|
|
@@ -514,7 +559,7 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
|
|
|
514
559
|
}
|
|
515
560
|
}
|
|
516
561
|
let result;
|
|
517
|
-
try { result = await renderFlight(request, { signal }); }
|
|
562
|
+
try { result = await renderFlight(request, { signal, deferFinalizer }); }
|
|
518
563
|
catch (error) {
|
|
519
564
|
const result = navigationResponse(error);
|
|
520
565
|
if (result) return { ...result, headers: { ...responseHeaders, ...result.headers, ...error.responseHeaders } };
|
|
@@ -531,7 +576,7 @@ async function renderAppPageInner({ modulePath, route = {}, manifest = {}, distD
|
|
|
531
576
|
let html, model, recovery = false;
|
|
532
577
|
try {
|
|
533
578
|
model = await decodeFlight(result.body, clientModules, distDir, { production });
|
|
534
|
-
html = await renderHtml(model.tree, model.router, { formState: model.formState, nonce: request.nonce, strictMode: manifest.config?.reactStrictMode !== false });
|
|
579
|
+
html = await renderHtml(model.tree, model.router, { timeoutMs: route.cacheConfig?.maxDuration ? route.cacheConfig.maxDuration * 1000 : undefined, formState: model.formState, nonce: request.nonce, strictMode: manifest.config?.reactStrictMode !== false });
|
|
535
580
|
} catch (error) {
|
|
536
581
|
if (error?.statusCode === 504) throw error;
|
|
537
582
|
const navigation = navigationResponse(error);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Cache only the compiler-owned, immutable routing scaffold. Component modules,
|
|
2
|
+
// configuration, request parameters and rendered trees are never memoized here.
|
|
3
|
+
const plans = new WeakMap();
|
|
4
|
+
const liveTrees = new WeakSet();
|
|
5
|
+
const ownValue = (object, name) => Object.getOwnPropertyDescriptor(object, name)?.value;
|
|
6
|
+
function frozenRecord(value) {
|
|
7
|
+
if (!value || typeof value !== 'object' || !Object.isFrozen(value)) return false;
|
|
8
|
+
const prototype = Object.getPrototypeOf(value);
|
|
9
|
+
if (Array.isArray(value) ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null) return false;
|
|
10
|
+
if (Array.isArray(value) && Object.hasOwn(value, Symbol.iterator)) return false;
|
|
11
|
+
return Reflect.ownKeys(value).every(key => 'value' in Object.getOwnPropertyDescriptor(value, key));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Mutable development/custom trees continue through the live traversal. */
|
|
15
|
+
export function appRoutePlan(root) {
|
|
16
|
+
if (!root || !Object.isFrozen(root)) return null;
|
|
17
|
+
const existing = plans.get(root);
|
|
18
|
+
if (existing) return existing;
|
|
19
|
+
if (liveTrees.has(root)) return null;
|
|
20
|
+
const pages = [], nodes = new Map(), rootLayouts = new Set();
|
|
21
|
+
let sequence = 0;
|
|
22
|
+
function visit(node, chain, branch, hasLayout) {
|
|
23
|
+
if (!frozenRecord(node)) return false;
|
|
24
|
+
const children = ownValue(node, 'children'), slots = ownValue(node, 'slots'), files = ownValue(node, 'files');
|
|
25
|
+
if (!Array.isArray(children) || !frozenRecord(children) || !frozenRecord(slots) || !frozenRecord(files)
|
|
26
|
+
|| typeof ownValue(node, 'path') !== 'string' || typeof ownValue(node, 'pattern') !== 'string'
|
|
27
|
+
|| typeof ownValue(node, 'segment') !== 'string' || nodes.has(node)) return false;
|
|
28
|
+
// Bound retained chains even for hand-written frozen manifests.
|
|
29
|
+
if (sequence >= 4096 || chain.length >= 64) return false;
|
|
30
|
+
const next = [...chain, node], start = sequence++;
|
|
31
|
+
const info = { start, end: start, depth: chain.length, branch, slots: Object.entries(slots), metadataSlots: Object.entries(slots).sort(([a], [b]) => a.localeCompare(b)) };
|
|
32
|
+
nodes.set(node, info);
|
|
33
|
+
if (files.layout && !hasLayout) rootLayouts.add(node.path);
|
|
34
|
+
if (files.page) pages.push({ node, chain: next, start, branch,
|
|
35
|
+
score: node.pattern.split('/').filter(Boolean).reduce((sum, value) => sum + (value.startsWith('[[...') ? 0 : value.startsWith('[...') ? 1 : value.startsWith('[') ? 3 : 6), 0) });
|
|
36
|
+
for (const child of children) if (!visit(child, next, branch, hasLayout || !!files.layout)) return false;
|
|
37
|
+
for (const child of Object.values(slots)) if (!visit(child, next, child, hasLayout || !!files.layout)) return false;
|
|
38
|
+
info.end = sequence;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
if (!visit(root, [], root, false)) { liveTrees.add(root); return null; }
|
|
42
|
+
const plan = { pages, nodes, rootLayouts };
|
|
43
|
+
plans.set(root, plan);
|
|
44
|
+
return plan;
|
|
45
|
+
}
|
|
@@ -1,26 +1,98 @@
|
|
|
1
1
|
// Route matching and navigation state do not need React or metadata rendering.
|
|
2
2
|
import { removeBasePath } from '../compat/paths.cjs';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
// Only trusted route syntax is cached, never URLs, decoded parameters or request
|
|
5
|
+
// state. Bound the shared lookup's entry count and retained text/segments.
|
|
6
|
+
const patterns = new Map();
|
|
7
|
+
// Live manifest entries own their parsed syntax weakly. Large route tables must
|
|
8
|
+
// not churn the bounded cross-call cache once for every saved parallel slot.
|
|
9
|
+
// Their lifetime follows the existing manifest; no old manifest is retained.
|
|
10
|
+
// Re-read the pattern on each use so mutable development manifests stay live.
|
|
11
|
+
const routePatterns = new WeakMap();
|
|
12
|
+
let patternCharacters = 0, patternSegments = 0;
|
|
13
|
+
function compilePattern(pattern) {
|
|
14
|
+
let compiled = patterns.get(pattern);
|
|
15
|
+
if (compiled) return compiled;
|
|
16
|
+
const segments = pattern.split('/').filter(Boolean);
|
|
17
|
+
const kinds = new Uint8Array(segments.length);
|
|
18
|
+
let score = 0;
|
|
19
|
+
for (let index = 0; index < segments.length; index++) {
|
|
20
|
+
const segment = segments[index];
|
|
21
|
+
if (segment[0] !== '[') { score += 6; continue; }
|
|
22
|
+
score += segment.startsWith('[[...') ? 0 : segment.startsWith('[...') ? 1 : 3;
|
|
11
23
|
const optional = /^\[\[\.\.\.([^\]]+)\]\]$/.exec(segment);
|
|
12
24
|
const catchAll = /^\[\.\.\.([^\]]+)\]$/.exec(segment);
|
|
13
25
|
if (optional || catchAll) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
26
|
+
kinds[index] = optional ? 3 : 2;
|
|
27
|
+
segments[index] = (optional || catchAll)[1];
|
|
28
|
+
} else {
|
|
29
|
+
const dynamic = /^\[([^\]]+)\]$/.exec(segment);
|
|
30
|
+
if (dynamic) { kinds[index] = 1; segments[index] = dynamic[1]; }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
compiled = { segments, kinds, score, characters: pattern.length };
|
|
34
|
+
if (pattern.length <= 2048 && segments.length <= 64) {
|
|
35
|
+
while (patterns.size >= 1024 || patternCharacters + pattern.length > 128 * 1024 || patternSegments + segments.length > 8192) {
|
|
36
|
+
const oldest = patterns.keys().next().value;
|
|
37
|
+
const removed = patterns.get(oldest);
|
|
38
|
+
patterns.delete(oldest);
|
|
39
|
+
patternCharacters -= removed.characters;
|
|
40
|
+
patternSegments -= removed.segments.length;
|
|
17
41
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
else if (segment !== parts[index]) return null;
|
|
42
|
+
patterns.set(pattern, compiled);
|
|
43
|
+
patternCharacters += pattern.length;
|
|
44
|
+
patternSegments += segments.length;
|
|
22
45
|
}
|
|
23
|
-
return
|
|
46
|
+
return compiled;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function pathSegments(pathname) {
|
|
50
|
+
const raw = pathname.split('/').filter(Boolean);
|
|
51
|
+
try { return raw.map(decodeURIComponent); }
|
|
52
|
+
catch { return null; }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function compiledRoute(route) {
|
|
56
|
+
const pattern = route.pattern;
|
|
57
|
+
const cached = routePatterns.get(route);
|
|
58
|
+
if (cached && cached.pattern === pattern) return cached.compiled;
|
|
59
|
+
const compiled = compilePattern(pattern);
|
|
60
|
+
if (pattern.length <= 2048 && compiled.segments.length <= 64) routePatterns.set(route, { pattern, compiled });
|
|
61
|
+
else routePatterns.delete(route);
|
|
62
|
+
return compiled;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function matchesParts({ segments, kinds }, parts, prefix = false, params) {
|
|
66
|
+
for (let index = 0; index < segments.length; index++) {
|
|
67
|
+
const kind = kinds[index];
|
|
68
|
+
if (kind >= 2) {
|
|
69
|
+
if (kind === 2 && index >= parts.length) return false;
|
|
70
|
+
if (params && index < parts.length) params[segments[index]] = parts.slice(index);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
if (index >= parts.length) return false;
|
|
74
|
+
if (kind === 1) { if (params) params[segments[index]] = parts[index]; }
|
|
75
|
+
else if (segments[index] !== parts[index]) return false;
|
|
76
|
+
}
|
|
77
|
+
return prefix || segments.length === parts.length;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function matchAppPattern(pattern, pathname, prefix = false) {
|
|
81
|
+
const compiled = compilePattern(pattern), parts = pathSegments(pathname);
|
|
82
|
+
if (!parts) return null;
|
|
83
|
+
const params = {};
|
|
84
|
+
return matchesParts(compiled, parts, prefix, params) ? params : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Decode a request pathname once; each match still owns its parameter values. */
|
|
88
|
+
export function createAppMatcher(pathname) {
|
|
89
|
+
const parts = pathSegments(pathname);
|
|
90
|
+
return (pattern, prefix = false) => {
|
|
91
|
+
const compiled = compilePattern(pattern);
|
|
92
|
+
if (!parts) return null;
|
|
93
|
+
const params = {};
|
|
94
|
+
return matchesParts(compiled, parts, prefix, params) ? params : null;
|
|
95
|
+
};
|
|
24
96
|
}
|
|
25
97
|
|
|
26
98
|
export function readRouterState(request) {
|
|
@@ -49,13 +121,21 @@ export function advancedRoutingSources(request, routes) {
|
|
|
49
121
|
const modules = new Set();
|
|
50
122
|
for (const saved of Object.values(state.slots)) {
|
|
51
123
|
const pathname = new URL(saved.url, 'http://prnext.local').pathname;
|
|
52
|
-
const
|
|
53
|
-
|
|
124
|
+
const parts = pathSegments(pathname);
|
|
125
|
+
if (!parts) continue;
|
|
126
|
+
let route, bestScore = -1;
|
|
127
|
+
for (let index = 0, length = routes.length; index < length; index++) {
|
|
128
|
+
if (!(index in routes)) continue;
|
|
129
|
+
const item = routes[index];
|
|
130
|
+
if (item.router !== 'app' || item.kind !== 'page' || item.internal) continue;
|
|
131
|
+
const compiled = compiledRoute(item);
|
|
132
|
+
// The original stable sort kept the first route when scores were equal.
|
|
133
|
+
// Lower-scoring routes cannot replace a match already found.
|
|
134
|
+
if (compiled.score <= bestScore || !matchesParts(compiled, parts)) continue;
|
|
135
|
+
route = item;
|
|
136
|
+
bestScore = compiled.score;
|
|
137
|
+
}
|
|
54
138
|
if (route) modules.add(route.module);
|
|
55
139
|
}
|
|
56
140
|
return [...modules];
|
|
57
141
|
}
|
|
58
|
-
|
|
59
|
-
function score(pattern) {
|
|
60
|
-
return pattern.split('/').filter(Boolean).reduce((sum, value) => sum + (value.startsWith('[[...') ? 0 : value.startsWith('[...') ? 1 : value.startsWith('[') ? 3 : 6), 0);
|
|
61
|
-
}
|