@thomas.f/prnext 0.1.0-alpha.1
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 +54 -0
- package/build/action-exports.mjs +107 -0
- package/build/actions.mjs +214 -0
- package/build/app-config.mjs +138 -0
- package/build/app-routing.mjs +119 -0
- package/build/app-static.mjs +158 -0
- package/build/app.mjs +536 -0
- package/build/cache-components.mjs +91 -0
- package/build/cache-handlers.mjs +54 -0
- package/build/check.mjs +40 -0
- package/build/compiler.mjs +222 -0
- package/build/config.mjs +193 -0
- package/build/css-modules.mjs +114 -0
- package/build/custom-routes.mjs +363 -0
- package/build/dev-worker.mjs +18 -0
- package/build/dev.mjs +76 -0
- package/build/directory-cache.mjs +29 -0
- package/build/dynamic.mjs +125 -0
- package/build/edge.mjs +141 -0
- package/build/font-data.LICENSE.txt +25 -0
- package/build/font-data.json +1 -0
- package/build/font-files.mjs +221 -0
- package/build/fonts.mjs +131 -0
- package/build/framework-imports.mjs +17 -0
- package/build/i18n.mjs +39 -0
- package/build/images-config.mjs +62 -0
- package/build/images.mjs +86 -0
- package/build/incremental.mjs +76 -0
- package/build/index.mjs +356 -0
- package/build/loader-cache.mjs +69 -0
- package/build/metadata-images.mjs +72 -0
- package/build/metadata-routes.mjs +77 -0
- package/build/middleware.mjs +231 -0
- package/build/migrate.mjs +289 -0
- package/build/module-resolution.mjs +78 -0
- package/build/native-discovery.mjs +134 -0
- package/build/native-packages.mjs +105 -0
- package/build/npm-packages.mjs +110 -0
- package/build/og.mjs +14 -0
- package/build/page-extensions.mjs +12 -0
- package/build/passthrough-loader.cjs +4 -0
- package/build/precompress.mjs +122 -0
- package/build/scan.mjs +178 -0
- package/build/script-workers.mjs +25 -0
- package/build/server-actions-config.mjs +27 -0
- package/build/source-maps.mjs +34 -0
- package/build/standalone-config.mjs +19 -0
- package/build/standalone-runner.mjs +21 -0
- package/build/standalone-worker.mjs +5 -0
- package/build/standalone.mjs +240 -0
- package/build/static-export.mjs +93 -0
- package/build/styles.mjs +174 -0
- package/build/transform-cache.mjs +50 -0
- package/build/transform.mjs +155 -0
- package/build/turbopack-conditions.mjs +59 -0
- package/build/webpack-bridge.cjs +3 -0
- package/build/webpack-graph.mjs +256 -0
- package/build/webpack-loader.cjs +9 -0
- package/cli.mjs +210 -0
- package/compat/app-client-page.cjs +32 -0
- package/compat/app-context.cjs +40 -0
- package/compat/app-context.d.cts +20 -0
- package/compat/app-dynamic.cjs +34 -0
- package/compat/app-error-boundary.cjs +53 -0
- package/compat/app-global-error-boundary.cjs +34 -0
- package/compat/app-layout-context.cjs +27 -0
- package/compat/app-navigation-boundary.cjs +47 -0
- package/compat/app.cjs +33 -0
- package/compat/app.d.cts +9 -0
- package/compat/cache-handlers.cjs +223 -0
- package/compat/cache-life.cjs +34 -0
- package/compat/cache.cjs +99 -0
- package/compat/cache.d.cts +36 -0
- package/compat/compat/router.d.cts +2 -0
- package/compat/compat-router.cjs +7 -0
- package/compat/constants.cjs +8 -0
- package/compat/constants.d.cts +7 -0
- package/compat/cookies.cjs +129 -0
- package/compat/data-cache.cjs +389 -0
- package/compat/dist/shared/lib/app-router-context.shared-runtime.d.cts +3 -0
- package/compat/dist/shared/lib/router-context.d.cts +1 -0
- package/compat/dist/shared/lib/router-context.shared-runtime.d.cts +3 -0
- package/compat/document.cjs +73 -0
- package/compat/document.d.cts +41 -0
- package/compat/draft.cjs +28 -0
- package/compat/dynamic-bailout.cjs +13 -0
- package/compat/dynamic.cjs +119 -0
- package/compat/dynamic.d.cts +32 -0
- package/compat/error.cjs +36 -0
- package/compat/error.d.cts +15 -0
- package/compat/fetch.d.cts +10 -0
- package/compat/font/google.d.cts +1 -0
- package/compat/font/local.d.cts +2 -0
- package/compat/font-google.cjs +2 -0
- package/compat/font-google.d.cts +1944 -0
- package/compat/font-local.cjs +5 -0
- package/compat/font-local.d.cts +3 -0
- package/compat/font-types.d.cts +25 -0
- package/compat/head.cjs +180 -0
- package/compat/head.d.cts +3 -0
- package/compat/headers.cjs +89 -0
- package/compat/headers.d.cts +26 -0
- package/compat/image-config.cjs +3 -0
- package/compat/image-imports.d.ts +12 -0
- package/compat/image-shared.cjs +62 -0
- package/compat/image.cjs +64 -0
- package/compat/image.d.cts +35 -0
- package/compat/incremental-cache.cjs +186 -0
- package/compat/index.d.ts +159 -0
- package/compat/instant-samples.cjs +39 -0
- package/compat/instant.cjs +13 -0
- package/compat/link.cjs +68 -0
- package/compat/link.d.cts +17 -0
- package/compat/locale.cjs +30 -0
- package/compat/metadata.d.cts +77 -0
- package/compat/navigation-controls.cjs +27 -0
- package/compat/navigation-server.cjs +25 -0
- package/compat/navigation.cjs +52 -0
- package/compat/navigation.d.cts +28 -0
- package/compat/next-app-router-context.cjs +6 -0
- package/compat/next-router-context.cjs +3 -0
- package/compat/og.cjs +32 -0
- package/compat/og.d.cts +1 -0
- package/compat/paths.cjs +33 -0
- package/compat/preview.cjs +41 -0
- package/compat/rewrite.cjs +41 -0
- package/compat/route-request.cjs +64 -0
- package/compat/router.cjs +137 -0
- package/compat/router.d.cts +58 -0
- package/compat/script-context.cjs +5 -0
- package/compat/script-loader.cjs +135 -0
- package/compat/script.cjs +69 -0
- package/compat/script.d.cts +17 -0
- package/compat/server-primitives.cjs +109 -0
- package/compat/server.cjs +13 -0
- package/compat/server.d.cts +37 -0
- package/compat/static-generation.cjs +108 -0
- package/compat/use-cache.cjs +129 -0
- package/index.mjs +1 -0
- package/native/platforms.json +6 -0
- package/native/resolve.mjs +81 -0
- package/package.json +127 -0
- package/release/verify.mjs +59 -0
- package/runtime/action-client.mjs +19 -0
- package/runtime/action-crypto.mjs +138 -0
- package/runtime/action-reference.mjs +2 -0
- package/runtime/action-server.mjs +161 -0
- package/runtime/action-ssr.mjs +5 -0
- package/runtime/api.mjs +326 -0
- package/runtime/app-client.mjs +539 -0
- package/runtime/app-content.mjs +8 -0
- package/runtime/app-errors.mjs +12 -0
- package/runtime/app-instant.mjs +30 -0
- package/runtime/app-metadata.mjs +410 -0
- package/runtime/app-partial-model.mjs +228 -0
- package/runtime/app-partial.mjs +130 -0
- package/runtime/app-prefetch.mjs +34 -0
- package/runtime/app-recovery.mjs +87 -0
- package/runtime/app-render.mjs +544 -0
- package/runtime/app-routing-access.mjs +143 -0
- package/runtime/app-routing-context.mjs +35 -0
- package/runtime/app-routing-state.mjs +61 -0
- package/runtime/app-routing.mjs +323 -0
- package/runtime/app-segments.mjs +22 -0
- package/runtime/app-static-params.mjs +120 -0
- package/runtime/app-static.mjs +86 -0
- package/runtime/app-stream-channel.mjs +92 -0
- package/runtime/app-stream-html.mjs +279 -0
- package/runtime/build-directory.mjs +48 -0
- package/runtime/cache-arguments.mjs +33 -0
- package/runtime/cache-components-rsc.mjs +64 -0
- package/runtime/cache-request.mjs +23 -0
- package/runtime/client-navigation.mjs +40 -0
- package/runtime/dev-client.mjs +129 -0
- package/runtime/dev-file-changes.mjs +110 -0
- package/runtime/document.mjs +96 -0
- package/runtime/edge.mjs +105 -0
- package/runtime/env.mjs +68 -0
- package/runtime/fetch-cache.mjs +379 -0
- package/runtime/font-preload.mjs +4 -0
- package/runtime/http.mjs +190 -0
- package/runtime/incremental-cache.mjs +83 -0
- package/runtime/metadata-route.mjs +83 -0
- package/runtime/middleware.mjs +186 -0
- package/runtime/module-loader.mjs +21 -0
- package/runtime/navigation.mjs +16 -0
- package/runtime/og-loader.mjs +17 -0
- package/runtime/pages-client.mjs +197 -0
- package/runtime/pages-error-props.mjs +6 -0
- package/runtime/pages-errors.mjs +85 -0
- package/runtime/pages-initial-props.mjs +43 -0
- package/runtime/pages-navigation.mjs +565 -0
- package/runtime/pages-paths.mjs +80 -0
- package/runtime/pages-revalidate.mjs +33 -0
- package/runtime/partial-artifact-cache.mjs +37 -0
- package/runtime/partial-template.mjs +174 -0
- package/runtime/profile.mjs +9 -0
- package/runtime/profiles.json +47 -0
- package/runtime/render.mjs +304 -0
- package/runtime/request-transport.mjs +41 -0
- package/runtime/request-work.mjs +7 -0
- package/runtime/route-static.mjs +117 -0
- package/runtime/rsc-worker.mjs +426 -0
- package/runtime/script-html.mjs +61 -0
- package/runtime/stream-select.mjs +25 -0
- package/runtime/stream-utils.mjs +127 -0
- package/runtime/transport.mjs +108 -0
- package/runtime/watch-project.mjs +130 -0
- package/runtime/worker-artifacts.mjs +22 -0
- package/runtime/worker-metadata.mjs +31 -0
- package/runtime/worker.mjs +197 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const list = value => value == null ? [] : Array.isArray(value) ? value : [value];
|
|
2
|
+
const xml = value => String(value).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
3
|
+
|
|
4
|
+
export function serializeMetadataRoute(kind, data) {
|
|
5
|
+
if (kind === 'manifest') return JSON.stringify(data);
|
|
6
|
+
if (kind === 'robots') {
|
|
7
|
+
if (!data || !data.rules) throw new TypeError('robots() must return an object with rules.');
|
|
8
|
+
let text = '';
|
|
9
|
+
for (const rule of list(data.rules)) {
|
|
10
|
+
for (const agent of list(rule.userAgent || '*')) text += `User-Agent: ${agent}\n`;
|
|
11
|
+
for (const [field, label] of [['allow', 'Allow'], ['disallow', 'Disallow']]) for (const item of list(rule[field])) text += `${label}: ${item}\n`;
|
|
12
|
+
if (rule.crawlDelay) text += `Crawl-delay: ${rule.crawlDelay}\n`;
|
|
13
|
+
for (const [key, values] of Object.entries(rule.other || {})) for (const value of list(values)) text += `${key}: ${value}\n`;
|
|
14
|
+
text += '\n';
|
|
15
|
+
}
|
|
16
|
+
if (data.host) text += `Host: ${data.host}\n`;
|
|
17
|
+
for (const url of list(data.sitemap)) text += `Sitemap: ${url}\n`;
|
|
18
|
+
return text;
|
|
19
|
+
}
|
|
20
|
+
if (kind !== 'sitemap' || !Array.isArray(data)) throw new TypeError('sitemap() must return an array.');
|
|
21
|
+
const namespaces = { image: 'http://www.google.com/schemas/sitemap-image/1.1', video: 'http://www.google.com/schemas/sitemap-video/1.1', xhtml: 'http://www.w3.org/1999/xhtml' };
|
|
22
|
+
const used = { image: data.some(item => item.images?.length), video: data.some(item => item.videos?.length), xhtml: data.some(item => Object.keys(item.alternates || {}).length) };
|
|
23
|
+
let text = '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"';
|
|
24
|
+
for (const [key, uri] of Object.entries(namespaces)) if (used[key]) text += ` xmlns:${key}="${uri}"`;
|
|
25
|
+
text += '>\n';
|
|
26
|
+
const tag = (name, value, attributes = '') => value == null ? '' : `<${name}${attributes}>${xml(value instanceof Date ? value.toISOString() : value)}</${name}>\n`;
|
|
27
|
+
for (const item of data) {
|
|
28
|
+
text += '<url>\n' + tag('loc', item.url);
|
|
29
|
+
for (const [language, url] of Object.entries(item.alternates?.languages || {})) text += `<xhtml:link rel="alternate" hreflang="${xml(language)}" href="${xml(url)}" />\n`;
|
|
30
|
+
for (const image of item.images || []) text += '<image:image>\n' + tag('image:loc', image) + '</image:image>\n';
|
|
31
|
+
for (const video of item.videos || []) {
|
|
32
|
+
text += '<video:video>\n';
|
|
33
|
+
for (const field of ['title', 'thumbnail_loc', 'description', 'content_loc', 'player_loc', 'duration', 'view_count', 'tag', 'rating', 'expiration_date', 'publication_date', 'family_friendly', 'requires_subscription', 'live']) text += tag('video:' + field, video[field]);
|
|
34
|
+
for (const field of ['restriction', 'platform', 'uploader']) if (video[field]) {
|
|
35
|
+
const attribute = field === 'uploader' ? 'info' : 'relationship';
|
|
36
|
+
text += tag('video:' + field, video[field].content, video[field][attribute] ? ` ${attribute}="${xml(video[field][attribute])}"` : '');
|
|
37
|
+
}
|
|
38
|
+
text += '</video:video>\n';
|
|
39
|
+
}
|
|
40
|
+
text += tag('lastmod', item.lastModified) + tag('changefreq', item.changeFrequency) + tag('priority', item.priority) + '</url>\n';
|
|
41
|
+
}
|
|
42
|
+
return text + '</urlset>\n';
|
|
43
|
+
}
|
|
44
|
+
export function validateMetadataVariants(rows, name) {
|
|
45
|
+
if (!Array.isArray(rows) || rows.length > 10000) throw new TypeError(`${name} must return an array with at most 10000 items`);
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
for (const item of rows) {
|
|
48
|
+
if (!item || !['string', 'number'].includes(typeof item.id) || !String(item.id) || /[/\\\x00-\x1f]/.test(String(item.id)) || ['.', '..'].includes(String(item.id))) throw new TypeError(`${name} requires a valid id for every item`);
|
|
49
|
+
const id = String(item.id);
|
|
50
|
+
if (seen.has(id)) throw new TypeError(`${name} returned duplicate id ${id}`);
|
|
51
|
+
seen.add(id);
|
|
52
|
+
}
|
|
53
|
+
return rows;
|
|
54
|
+
}
|
|
55
|
+
export function metadataHandler(source, { kind, image, multiple, contentType }) {
|
|
56
|
+
if (typeof source.default !== 'function') throw new TypeError(`${kind} metadata file must export a default function`);
|
|
57
|
+
if (['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'].some(name => name in source)) throw new Error('Metadata files must export a default metadata function, not HTTP methods');
|
|
58
|
+
return async (_request, context) => {
|
|
59
|
+
const { __metadata_id__: rawId, ...params } = await context.params;
|
|
60
|
+
const id = kind === 'sitemap' ? typeof rawId === 'string' && rawId.endsWith('.xml') ? rawId.slice(0, -4) : undefined : rawId;
|
|
61
|
+
if (multiple) {
|
|
62
|
+
const name = image ? 'generateImageMetadata' : 'generateSitemaps';
|
|
63
|
+
const rows = validateMetadataVariants(await source[name]({ params }), name);
|
|
64
|
+
if (!rows.some(item => String(item.id) === id)) return new Response('Not Found', { status: 404 });
|
|
65
|
+
}
|
|
66
|
+
const result = await source.default({ params: Promise.resolve(params), ...(multiple ? { id: Promise.resolve(id) } : {}) });
|
|
67
|
+
if (image) {
|
|
68
|
+
if (!(result instanceof Response)) throw new TypeError(`${kind} must return a Response or ImageResponse`);
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
return new Response(serializeMetadataRoute(kind, result), { headers: { 'content-type': contentType } });
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export async function metadataStaticParams(source, { kind, image }, { params = {} } = {}) {
|
|
75
|
+
const parents = source.generateStaticParams ? await source.generateStaticParams({ params }) : [params];
|
|
76
|
+
const result = [];
|
|
77
|
+
const name = image ? 'generateImageMetadata' : 'generateSitemaps';
|
|
78
|
+
for (const parent of parents) for (const item of validateMetadataVariants(await source[name]({ params: parent }), name)) {
|
|
79
|
+
result.push({ ...parent, __metadata_id__: String(item.id) + (kind === 'sitemap' ? '.xml' : '') });
|
|
80
|
+
if (result.length > 10000) throw new Error('Metadata path generation exceeds 10000 paths');
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { trackRequestWork } from './request-work.mjs';
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
3
|
+
import { loadModule } from './module-loader.mjs';
|
|
4
|
+
import { NextRequest, NextResponse } from '../compat/server.cjs';
|
|
5
|
+
import { removeBasePath } from '../compat/paths.cjs';
|
|
6
|
+
import { runRequestContext, currentRequest, runWithoutRequestContext } from '../compat/headers.cjs';
|
|
7
|
+
import { flushCacheWork } from '../compat/data-cache.cjs';
|
|
8
|
+
import { apiTimeoutError, streamingBody, withSignal } from './stream-utils.mjs';
|
|
9
|
+
import { installFetchCache } from './fetch-cache.mjs';
|
|
10
|
+
|
|
11
|
+
installFetchCache();
|
|
12
|
+
|
|
13
|
+
const FLIGHT_HEADERS = ['rsc', 'next-router-state-tree', 'x-prnext-router-state', 'next-router-prefetch', 'next-hmr-refresh', 'next-router-segment-prefetch'];
|
|
14
|
+
const HOP_HEADERS = new Set(['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade', 'content-length']);
|
|
15
|
+
const MAX_BODY = 8 * 1024 * 1024;
|
|
16
|
+
const MAX_SCOPES = 32;
|
|
17
|
+
const MAX_PROMISES = 128;
|
|
18
|
+
const scopes = new Set();
|
|
19
|
+
let pendingPromises = 0;
|
|
20
|
+
|
|
21
|
+
function observeBackground(value, entry) {
|
|
22
|
+
// Keep the reaction detached from the scope closure. A user promise may
|
|
23
|
+
// remain reachable forever; clearing entry.settle after its deadline must
|
|
24
|
+
// release the scope and request while still observing late rejections.
|
|
25
|
+
// Otherwise the child promise itself inherits the request store even after
|
|
26
|
+
// the detachable callback has been cleared. User work keeps its own context.
|
|
27
|
+
runWithoutRequestContext(() => {
|
|
28
|
+
void Promise.resolve(value).then(() => entry.settle?.(), error => entry.settle?.(error));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function backgroundScope({ timeoutMs, onTimeout, onError }) {
|
|
33
|
+
const entries = new Set();
|
|
34
|
+
let active = false, completed = false, closed = false, timer, resolveDone;
|
|
35
|
+
const scope = { done: new Promise(resolve => { resolveDone = resolve; }) };
|
|
36
|
+
function close() {
|
|
37
|
+
if (closed) return;
|
|
38
|
+
closed = true;
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
for (const entry of entries) { entry.settle = undefined; pendingPromises--; }
|
|
41
|
+
entries.clear(); scopes.delete(scope); resolveDone();
|
|
42
|
+
}
|
|
43
|
+
function settle(entry, error) {
|
|
44
|
+
if (!entry.settle) return;
|
|
45
|
+
entry.settle = undefined; entries.delete(entry); pendingPromises--;
|
|
46
|
+
if (error) onError(error);
|
|
47
|
+
if (completed && !entries.size) close();
|
|
48
|
+
}
|
|
49
|
+
scope.waitUntil = value => {
|
|
50
|
+
// Observe even refused registrations: callers often construct a rejected
|
|
51
|
+
// promise before invoking waitUntil, which must not become unhandled.
|
|
52
|
+
const entry = { settle: undefined };
|
|
53
|
+
observeBackground(value, entry);
|
|
54
|
+
if (closed) throw new Error('waitUntil cannot be called after its middleware invocation has completed');
|
|
55
|
+
if (pendingPromises >= MAX_PROMISES || (!active && scopes.size >= MAX_SCOPES)) throw new Error('Middleware waitUntil capacity exceeded');
|
|
56
|
+
if (!active) {
|
|
57
|
+
active = true; scopes.add(scope);
|
|
58
|
+
timer = setTimeout(() => {
|
|
59
|
+
const error = apiTimeoutError('middleware waitUntil', timeoutMs);
|
|
60
|
+
close(); onTimeout(error); onError(error);
|
|
61
|
+
}, timeoutMs);
|
|
62
|
+
}
|
|
63
|
+
entry.settle = error => settle(entry, error);
|
|
64
|
+
entries.add(entry); pendingPromises++;
|
|
65
|
+
};
|
|
66
|
+
scope.complete = () => { completed = true; if (!entries.size) close(); };
|
|
67
|
+
return scope;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Test/shutdown hooks inspect only framework-owned, bounded work. Arbitrary
|
|
71
|
+
// application promises cannot be forcibly canceled after their deadline.
|
|
72
|
+
export function middlewareBackgroundState() { return { scopes: scopes.size, promises: pendingPromises }; }
|
|
73
|
+
export async function drainMiddlewareWork() { await Promise.all([...scopes].map(scope => scope.done)); }
|
|
74
|
+
|
|
75
|
+
function visibleHeaders(values, normalize) {
|
|
76
|
+
const headers = new Headers(values);
|
|
77
|
+
for (const name of [...headers.keys()]) if (name.startsWith('x-middleware-') || name === 'x-prnext-rewrite') headers.delete(name);
|
|
78
|
+
if (normalize) for (const name of FLIGHT_HEADERS) headers.delete(name);
|
|
79
|
+
return headers;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function runMiddleware(options) {
|
|
83
|
+
const metadata = options.manifest?.middleware || options.middleware || {};
|
|
84
|
+
const production = options.production ?? true;
|
|
85
|
+
const timeoutMs = options.timeoutMs ?? 25_000;
|
|
86
|
+
const normalize = !(options.manifest?.config?.skipProxyUrlNormalize ?? options.manifest?.config?.skipMiddlewareUrlNormalize);
|
|
87
|
+
const exposedUrl = new URL(!normalize && options.originalUrl ? options.originalUrl : options.url);
|
|
88
|
+
if (normalize) exposedUrl.searchParams.delete('_rsc');
|
|
89
|
+
const headers = visibleHeaders(options.headers || {}, normalize);
|
|
90
|
+
const controller = new AbortController();
|
|
91
|
+
const aborted = () => controller.abort(options.signal.reason);
|
|
92
|
+
if (options.signal?.aborted) aborted();
|
|
93
|
+
else options.signal?.addEventListener('abort', aborted, { once: true });
|
|
94
|
+
const headersTimer = setTimeout(() => controller.abort(apiTimeoutError('middleware response headers', timeoutMs)), timeoutMs);
|
|
95
|
+
let responseComplete = false;
|
|
96
|
+
let backgroundTimeout;
|
|
97
|
+
let finalizeInvocation;
|
|
98
|
+
const report = options.onBackgroundError || (error => console.error('[prnext] Middleware background work failed:', error?.stack || error));
|
|
99
|
+
const background = backgroundScope({ timeoutMs: options.waitUntilTimeoutMs ?? 25_000,
|
|
100
|
+
onError: report, onTimeout(error) { backgroundTimeout = error; if (responseComplete) controller.abort(error); } });
|
|
101
|
+
const finishResponse = () => {
|
|
102
|
+
responseComplete = true;
|
|
103
|
+
options.signal?.removeEventListener('abort', aborted);
|
|
104
|
+
if (backgroundTimeout) controller.abort(backgroundTimeout);
|
|
105
|
+
finalizeInvocation?.();
|
|
106
|
+
};
|
|
107
|
+
let output;
|
|
108
|
+
try {
|
|
109
|
+
const basePath = options.manifest?.config?.basePath || '';
|
|
110
|
+
const internalUrl = new URL(exposedUrl);
|
|
111
|
+
internalUrl.pathname = removeBasePath(internalUrl.pathname, basePath);
|
|
112
|
+
return await runRequestContext({ url: internalUrl.href, basePath, method: options.method || 'GET', headers,
|
|
113
|
+
phase: 'middleware', production, mutableCookies: true, signal: controller.signal }, async () => {
|
|
114
|
+
const context = currentRequest();
|
|
115
|
+
let result;
|
|
116
|
+
let finalized = false;
|
|
117
|
+
finalizeInvocation = () => {
|
|
118
|
+
if (finalized) return;
|
|
119
|
+
finalized = true;
|
|
120
|
+
context.cookies._mutable = false;
|
|
121
|
+
context.cacheState.closed = true;
|
|
122
|
+
if (context.cacheState.pending.size) {
|
|
123
|
+
try { background.waitUntil(flushCacheWork(context)); }
|
|
124
|
+
catch (error) { report(error); }
|
|
125
|
+
}
|
|
126
|
+
background.complete();
|
|
127
|
+
};
|
|
128
|
+
try {
|
|
129
|
+
controller.signal.throwIfAborted();
|
|
130
|
+
const module = await withSignal(loadModule(options.modulePath), controller.signal);
|
|
131
|
+
const exportName = metadata.exportName || metadata.convention || 'middleware';
|
|
132
|
+
const handler = module[exportName];
|
|
133
|
+
if (typeof handler !== 'function') throw new TypeError(`Middleware must export a ${exportName} function`);
|
|
134
|
+
const init = { method: options.method || 'GET', headers, signal: controller.signal };
|
|
135
|
+
if (!['GET', 'HEAD'].includes(init.method)) {
|
|
136
|
+
if (typeof options.body === 'string' && options.body.length > Math.ceil(MAX_BODY / 3) * 4) throw Object.assign(new Error('Middleware request exceeds the 8 MiB PRNext limit'), { statusCode: 413 });
|
|
137
|
+
const body = Buffer.isBuffer(options.body) ? options.body : Buffer.from(options.body || '', 'base64');
|
|
138
|
+
if (body.byteLength > MAX_BODY) throw Object.assign(new Error('Middleware request exceeds the 8 MiB PRNext limit'), { statusCode: 413 });
|
|
139
|
+
if (body.byteLength) init.body = body;
|
|
140
|
+
}
|
|
141
|
+
const request = new NextRequest(exposedUrl, { ...init, nextConfig: options.manifest?.config });
|
|
142
|
+
const event = { sourcePage: `/${metadata.convention || 'middleware'}`,
|
|
143
|
+
waitUntil(value) { trackRequestWork(value, options.signal); background.waitUntil(value); }, passThroughOnException() {},
|
|
144
|
+
get request() { throw new Error('Read the request from the middleware function first argument'); },
|
|
145
|
+
respondWith() { throw new Error('Return a Response from the middleware function instead of respondWith()'); } };
|
|
146
|
+
const pending = trackRequestWork(Promise.resolve().then(() => handler(request, event)), options.signal);
|
|
147
|
+
void pending.then(value => { if (controller.signal.aborted && value instanceof Response) void value.body?.cancel(controller.signal.reason).catch(() => {}); }, () => {});
|
|
148
|
+
result = await withSignal(pending, controller.signal);
|
|
149
|
+
if (result === null || result === undefined) result = NextResponse.next();
|
|
150
|
+
if (!(result instanceof Response)) throw new TypeError('Middleware must return a Response, null, or undefined');
|
|
151
|
+
clearTimeout(headersTimer);
|
|
152
|
+
const responseHeaders = Object.fromEntries(result.headers);
|
|
153
|
+
const cookies = [...result.headers.getSetCookie(), ...context.outgoingCookies.values()];
|
|
154
|
+
if (cookies.length) responseHeaders['set-cookie'] = cookies;
|
|
155
|
+
const nominated = String(responseHeaders.connection || '').split(',').map(name => name.trim().toLowerCase());
|
|
156
|
+
for (const name of Object.keys(responseHeaders)) if (HOP_HEADERS.has(name) || nominated.includes(name)) delete responseHeaders[name];
|
|
157
|
+
if (Buffer.byteLength(JSON.stringify(responseHeaders)) > 60 * 1024) throw new Error('Middleware response headers exceed the PRNext metadata limit');
|
|
158
|
+
// Match native/Next routing precedence: rewrite, then Location, then
|
|
159
|
+
// continuation. A Location-bearing final response may have a body.
|
|
160
|
+
const control = responseHeaders['x-middleware-rewrite'] ||
|
|
161
|
+
(!Object.hasOwn(responseHeaders, 'location') && responseHeaders['x-middleware-next']);
|
|
162
|
+
const noBody = control || init.method === 'HEAD' || [204, 205, 304].includes(result.status);
|
|
163
|
+
if (noBody || !result.body) {
|
|
164
|
+
void result.body?.cancel().catch(() => {});
|
|
165
|
+
finishResponse();
|
|
166
|
+
output = { status: result.status, headers: responseHeaders, body: Buffer.alloc(0), cancel() {} };
|
|
167
|
+
} else {
|
|
168
|
+
const body = streamingBody(result.body, { signal: controller.signal, timeoutMs,
|
|
169
|
+
runInContext: AsyncLocalStorage.snapshot(), onCancel: reason => controller.abort(reason), onComplete: finishResponse });
|
|
170
|
+
output = { status: result.status, headers: responseHeaders, body, cancel: reason => body.cancel(reason) };
|
|
171
|
+
}
|
|
172
|
+
context.cookies._mutable = false;
|
|
173
|
+
context.cacheState.closed = true;
|
|
174
|
+
return { ...output, finalizeCache: finalizeInvocation };
|
|
175
|
+
} catch (error) {
|
|
176
|
+
controller.abort(error);
|
|
177
|
+
void result?.body?.cancel(error).catch(() => {});
|
|
178
|
+
finalizeInvocation();
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
} finally {
|
|
183
|
+
clearTimeout(headersTimer);
|
|
184
|
+
if (!output) finishResponse();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
// Node already retains ESM namespaces. Keep only a bounded index of resolved
|
|
6
|
+
// Pages modules, never import promises (which can retain a request's ALS frame).
|
|
7
|
+
const resolvedPages = new Map();
|
|
8
|
+
export function loadedPagesModule(modulePath) { return resolvedPages.get(modulePath); }
|
|
9
|
+
|
|
10
|
+
export async function loadModule(modulePath, rememberPage = false) {
|
|
11
|
+
// CJS must keep honoring application changes to require.cache/exports.
|
|
12
|
+
if (modulePath.endsWith('.cjs')) return require(modulePath);
|
|
13
|
+
const imported = import(pathToFileURL(modulePath).href);
|
|
14
|
+
if (!rememberPage) return imported;
|
|
15
|
+
const namespace = await imported;
|
|
16
|
+
if (!resolvedPages.has(modulePath)) {
|
|
17
|
+
if (resolvedPages.size >= 256) resolvedPages.delete(resolvedPages.keys().next().value);
|
|
18
|
+
resolvedPages.set(modulePath, namespace);
|
|
19
|
+
}
|
|
20
|
+
return namespace;
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Decode Next's navigation control signals without treating user errors as 500s. */
|
|
2
|
+
export function navigationResponse(error) {
|
|
3
|
+
const digest = error?.digest;
|
|
4
|
+
if (digest === 'NEXT_HTTP_ERROR_FALLBACK;404') return { status: 404, headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' }, body: Buffer.from('Not Found') };
|
|
5
|
+
if (typeof digest !== 'string' || !digest.startsWith('NEXT_REDIRECT;')) return null;
|
|
6
|
+
const parts = digest.split(';');
|
|
7
|
+
if (parts.at(-1) === '') parts.pop();
|
|
8
|
+
const status = Number(parts.pop());
|
|
9
|
+
const destination = parts.slice(2).join(';');
|
|
10
|
+
if (![307, 308].includes(status) || !destination || /[\r\n]/.test(destination)) return null;
|
|
11
|
+
let parsed;
|
|
12
|
+
try { parsed = new URL(destination, 'http://prnext.invalid'); }
|
|
13
|
+
catch { return null; }
|
|
14
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) return null;
|
|
15
|
+
return { status, headers: { location: destination, 'cache-control': 'no-store' }, body: Buffer.alloc(0) };
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Upstream's Node bundle contains HarfBuzz's CommonJS globals and an adjacent
|
|
2
|
+
// hb.wasm reference. Resolve these explicitly for direct prnext/og consumers.
|
|
3
|
+
// Production builds prepare a relocatable module instead and avoid this loader.
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { readFile } from 'node:fs/promises';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
export async function loadImageRenderer() {
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const entry = require.resolve('@vercel/og');
|
|
11
|
+
const fontDirectory = path.dirname(entry);
|
|
12
|
+
const satori = createRequire(entry).resolve('satori');
|
|
13
|
+
const harfbuzzDirectory = path.dirname(createRequire(satori).resolve('harfbuzzjs/hb.wasm'));
|
|
14
|
+
const source = (await readFile(entry, 'utf8')).replaceAll('import.meta.url', JSON.stringify(pathToFileURL(entry).href));
|
|
15
|
+
const globals = `import {createRequire as __prnextRequire} from 'node:module';const require=__prnextRequire(${JSON.stringify(pathToFileURL(path.join(fontDirectory, 'index.node.js')).href)}),__filename=${JSON.stringify(entry)},__dirname=${JSON.stringify(harfbuzzDirectory)};\n`;
|
|
16
|
+
return import('data:text/javascript;base64,' + Buffer.from(globals + source).toString('base64'));
|
|
17
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { hydrateRoot } from 'react-dom/client';
|
|
3
|
+
import Router, { RouterProvider, useRouter, installPagesRouter } from '../compat/router.cjs';
|
|
4
|
+
import { createPagesNavigation, pageRouterSnapshot } from './pages-navigation.mjs';
|
|
5
|
+
import { HeadProvider } from '../compat/head.cjs';
|
|
6
|
+
import { preloadReady } from '../compat/dynamic.cjs';
|
|
7
|
+
import { readRewriteMarker, readRewriteHeader } from '../compat/rewrite.cjs';
|
|
8
|
+
import { pageDataRedirectURL } from './client-navigation.mjs';
|
|
9
|
+
import { addBasePath, removeBasePath } from '../compat/paths.cjs';
|
|
10
|
+
import ErrorPage from '../compat/error.cjs';
|
|
11
|
+
import { ScriptContext } from '../compat/script-context.cjs';
|
|
12
|
+
import { initScriptLoader } from '../compat/script-loader.cjs';
|
|
13
|
+
|
|
14
|
+
const MAX_DATA_BYTES = 16 * 1024 * 1024;
|
|
15
|
+
|
|
16
|
+
export function pageDataURL(buildId, location, basePath = '') {
|
|
17
|
+
if (typeof buildId !== 'string' || !buildId) throw new Error('Missing PRNext build ID');
|
|
18
|
+
const url = new URL(location);
|
|
19
|
+
const pathname = removeBasePath(url.pathname, basePath).replace(/\/+$/, '') || '/';
|
|
20
|
+
// /index.json represents the root; literal /index routes need one escape prefix.
|
|
21
|
+
const firstSegment = decodeURIComponent(pathname.split('/')[1] || '');
|
|
22
|
+
const asset = pathname === '/' ? '/index' : firstSegment === 'index' ? `/index${pathname}` : pathname;
|
|
23
|
+
return `${url.origin}${addBasePath(`/_prnext/data/${encodeURIComponent(buildId)}${asset}.json`, basePath)}${url.search}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function readPageData(response, limit = MAX_DATA_BYTES, { discard = false, signal } = {}) {
|
|
27
|
+
const error = !response.ok && (discard || response.status !== 404)
|
|
28
|
+
? new Error(`Page data request failed (${response.status})`)
|
|
29
|
+
: !discard && !response.headers.get('content-type')?.toLowerCase().includes('application/json')
|
|
30
|
+
? new Error('Invalid page data response') : null;
|
|
31
|
+
if (error) {
|
|
32
|
+
await response.body?.cancel(error).catch(() => {});
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
signal?.throwIfAborted();
|
|
36
|
+
const reader = response.body?.getReader();
|
|
37
|
+
if (!reader) {
|
|
38
|
+
if (discard) return { data: null, bytes: 0 };
|
|
39
|
+
throw new Error('Empty page data response');
|
|
40
|
+
}
|
|
41
|
+
const cancel = () => { void reader.cancel(signal.reason).catch(() => {}); };
|
|
42
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
43
|
+
const decoder = discard ? null : new TextDecoder();
|
|
44
|
+
let length = 0;
|
|
45
|
+
let source = '';
|
|
46
|
+
try {
|
|
47
|
+
while (true) {
|
|
48
|
+
const { done, value } = await reader.read();
|
|
49
|
+
signal?.throwIfAborted();
|
|
50
|
+
if (done) break;
|
|
51
|
+
length += value.byteLength;
|
|
52
|
+
if (length > limit) throw new Error('Page data exceeds the response limit');
|
|
53
|
+
if (decoder) source += decoder.decode(value, { stream: true });
|
|
54
|
+
}
|
|
55
|
+
if (discard) return { data: null, bytes: length };
|
|
56
|
+
source += decoder.decode();
|
|
57
|
+
return { data: JSON.parse(source), bytes: length };
|
|
58
|
+
} catch (error) {
|
|
59
|
+
await reader.cancel(error).catch(() => {});
|
|
60
|
+
throw error;
|
|
61
|
+
} finally { signal?.removeEventListener('abort', cancel); reader.releaseLock(); }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function readPageDataResponse(response, visibleURL, {
|
|
65
|
+
maxBytes = MAX_DATA_BYTES, includeBytes = false, fetcher = globalThis.fetch, signal, requestURL = visibleURL,
|
|
66
|
+
} = {}) {
|
|
67
|
+
try {
|
|
68
|
+
let legacy = false;
|
|
69
|
+
for (let redirects = 0; response.headers.get('x-prnext-legacy-navigation') === '1'; redirects++) {
|
|
70
|
+
legacy = true;
|
|
71
|
+
const location = response.headers.get('x-prnext-legacy-location');
|
|
72
|
+
if (location === null) break;
|
|
73
|
+
if (!response.ok) throw new Error(`Page data request failed (${response.status})`);
|
|
74
|
+
if (redirects === 20) throw new Error('Too many legacy page redirects');
|
|
75
|
+
// Location is an HTTP URL relative to the actual data response, not the
|
|
76
|
+
// browser's visible route. Only the final response may supply a rewrite.
|
|
77
|
+
const target = new URL(location, response.url || requestURL);
|
|
78
|
+
if (!['http:', 'https:'].includes(target.protocol)) throw new Error('Unsupported navigation protocol');
|
|
79
|
+
await response.body?.cancel();
|
|
80
|
+
signal?.throwIfAborted();
|
|
81
|
+
response = await fetcher(target.href, { credentials: 'same-origin', redirect: 'follow', signal,
|
|
82
|
+
headers: { accept: 'application/json', 'x-nextjs-data': '1', 'x-prnext-navigation': '1' } });
|
|
83
|
+
requestURL = target.href;
|
|
84
|
+
}
|
|
85
|
+
if (legacy && !response.ok && (response.status < 300 || response.status >= 400)) throw new Error(`Page data request failed (${response.status})`);
|
|
86
|
+
const redirect = pageDataRedirectURL(response, visibleURL);
|
|
87
|
+
if (redirect) {
|
|
88
|
+
await response.body?.cancel();
|
|
89
|
+
return { redirect: redirect.href };
|
|
90
|
+
}
|
|
91
|
+
// Legacy middleware probes run server hooks for their effects. Their body
|
|
92
|
+
// does not provide props: the client hook runs after routing is resolved.
|
|
93
|
+
const result = await readPageData(response, maxBytes, { discard: legacy, signal });
|
|
94
|
+
const rewrite = readRewriteHeader(response) || result.data?.__PRNEXT_ROUTER__?.rewrite;
|
|
95
|
+
return { data: result.data, rewrite, ...(legacy ? { legacy: true } : {}), ...(includeBytes ? { bytes: result.bytes,
|
|
96
|
+
cacheable: !legacy && !rewrite && !/(?:^|[,\s])(?:no-store|private)(?:[,\s=]|$)/i.test(response.headers.get('cache-control') || ''),
|
|
97
|
+
} : {}) };
|
|
98
|
+
} catch (error) {
|
|
99
|
+
await response.body?.cancel(error).catch(() => {});
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function PageEntry({ Page, App, data }) {
|
|
105
|
+
const router = useRouter();
|
|
106
|
+
return App
|
|
107
|
+
? React.createElement(App, { ...data.appProps, Component: Page, pageProps: data.props, router })
|
|
108
|
+
: React.createElement(Page, data.props);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
class PagesErrorBoundary extends React.Component {
|
|
112
|
+
state = { view: this.props.view, error: null };
|
|
113
|
+
static getDerivedStateFromProps(props, state) {
|
|
114
|
+
return props.view === state.view ? null : { view: props.view, error: null };
|
|
115
|
+
}
|
|
116
|
+
static getDerivedStateFromError(error) { return { error }; }
|
|
117
|
+
componentDidCatch(error) {
|
|
118
|
+
if (process.env.NODE_ENV === 'development') globalThis.__PRNEXT_DEV__?.reportError(error);
|
|
119
|
+
void this.props.reportError(error, this.props.view);
|
|
120
|
+
}
|
|
121
|
+
render() {
|
|
122
|
+
return this.state.error ? React.createElement(ErrorPage, { statusCode: this.state.error.statusCode }) : this.props.children;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function PagesRoot({ state, committed, reportError, strictMode }) {
|
|
127
|
+
const [hydrated, setHydrated] = React.useState(false);
|
|
128
|
+
React.useLayoutEffect(() => { setHydrated(true); committed?.(); }, [state, committed]);
|
|
129
|
+
// Static HTML needs the visible query once after hydration. Subsequent
|
|
130
|
+
// navigation snapshots already distinguish href's query from the visible as URL.
|
|
131
|
+
const router = hydrated && !state.clientSnapshot ? pageRouterSnapshot(state.router, new URL(window.location.href), new URL(window.location.href), state.rewrite || state.router.rewrite) : state.router;
|
|
132
|
+
return React.createElement(RouterProvider, { router, managed: true },
|
|
133
|
+
React.createElement(HeadProvider, { restoreDefaults: true },
|
|
134
|
+
React.createElement(ScriptContext.Provider, { value: { appDir: false, ssr: !hydrated } },
|
|
135
|
+
React.createElement(PagesErrorBoundary, { view: state, reportError }, React.createElement(PageEntry, { Page: state.Page, App: state.App, data: state })))));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function pagesView(props) {
|
|
139
|
+
return React.createElement(props.strictMode ? React.StrictMode : React.Fragment, null, React.createElement(PagesRoot, props));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let application;
|
|
143
|
+
async function startPages({ Page, App, pattern, manifestUrl, basePath = '', trailingSlash = false, skipTrailingSlashRedirect = false, assetBase, strictMode = false }) {
|
|
144
|
+
const data = window.__PRNEXT_DATA__;
|
|
145
|
+
if (!data?.router || !data.props) throw new Error('Missing PRNext page data');
|
|
146
|
+
const rewrite = readRewriteMarker(document);
|
|
147
|
+
const initial = { ...data, router: { ...data.router, basePath, trailingSlash, skipTrailingSlashRedirect }, Page, App, ...(rewrite ? { rewrite } : {}) };
|
|
148
|
+
let buildId = data.buildId;
|
|
149
|
+
const container = document.getElementById('__prnext');
|
|
150
|
+
if (!container) throw new Error('Missing PRNext page root');
|
|
151
|
+
const scripts = document.getElementById('__PRNEXT_SCRIPT_LOADER__');
|
|
152
|
+
initScriptLoader(scripts ? JSON.parse(scripts.textContent) : []);
|
|
153
|
+
scripts?.remove();
|
|
154
|
+
await preloadReady(data.dynamicIds || []);
|
|
155
|
+
let root;
|
|
156
|
+
const controller = createPagesNavigation({ initial, initialRoute: { pattern: pattern || data.router.pathname }, manifestUrl, basePath, trailingSlash, skipTrailingSlashRedirect, assetBase,
|
|
157
|
+
events: Router.events,
|
|
158
|
+
commit(state) { return new Promise(resolve => { root.render(pagesView({ state, strictMode, committed: resolve, reportError: controller.reportRenderError })); }); },
|
|
159
|
+
async readData(url, signal, { speculative, fetcher }) {
|
|
160
|
+
const abort = new AbortController();
|
|
161
|
+
const cancel = () => abort.abort(signal.reason);
|
|
162
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
163
|
+
if (signal.aborted) cancel();
|
|
164
|
+
const timer = setTimeout(() => abort.abort(new Error('Page data request timed out')), 30_000);
|
|
165
|
+
try {
|
|
166
|
+
const requestURL = pageDataURL(buildId, url.href, basePath);
|
|
167
|
+
const response = await fetcher(requestURL, {
|
|
168
|
+
credentials: 'same-origin', redirect: 'manual', signal: abort.signal,
|
|
169
|
+
headers: { accept: 'application/json', 'x-nextjs-data': '1', 'x-prnext-navigation': '1', ...(speculative ? { purpose: 'prefetch' } : {}) },
|
|
170
|
+
});
|
|
171
|
+
return await readPageDataResponse(response, url.href, { maxBytes: speculative ? 2 * 1024 * 1024 : MAX_DATA_BYTES,
|
|
172
|
+
includeBytes: speculative, fetcher, signal: abort.signal, requestURL });
|
|
173
|
+
} finally { clearTimeout(timer); signal.removeEventListener('abort', cancel); }
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
installPagesRouter(controller);
|
|
177
|
+
let resolveHydration;
|
|
178
|
+
const hydrated = new Promise(resolve => { resolveHydration = resolve; });
|
|
179
|
+
root = hydrateRoot(container, pagesView({ state: initial, strictMode, committed: resolveHydration, reportError: controller.reportRenderError }));
|
|
180
|
+
await hydrated;
|
|
181
|
+
if (data.router.isFallback) void controller.refreshFallback();
|
|
182
|
+
return { root, router: controller, ...(process.env.NODE_ENV === 'development' ? {
|
|
183
|
+
async devCommit(options) {
|
|
184
|
+
buildId = options.dev.buildId;
|
|
185
|
+
window.__PRNEXT_DATA__.buildId = buildId;
|
|
186
|
+
await controller.devRefresh({ buildId, manifestUrl: options.manifestUrl });
|
|
187
|
+
},
|
|
188
|
+
} : {}) };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function bootstrapPages(options) {
|
|
192
|
+
if (process.env.NODE_ENV === 'development' && options.dev && globalThis.__PRNEXT_DEV__) {
|
|
193
|
+
return globalThis.__PRNEXT_DEV__.bootstrap('pages', options, () => startPages(options));
|
|
194
|
+
}
|
|
195
|
+
application ||= startPages(options);
|
|
196
|
+
return application;
|
|
197
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { loadPageInitialProps } from './pages-initial-props.mjs';
|
|
2
|
+
|
|
3
|
+
/** Error hooks use the same App delegation as ordinary legacy Pages. */
|
|
4
|
+
export async function errorInitialProps(Page, context, App, snapshot) {
|
|
5
|
+
return (await loadPageInitialProps({ Page, App, context, routerSnapshot: snapshot })).props;
|
|
6
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {localePath} from '../compat/locale.cjs';
|
|
3
|
+
import {removeBasePath} from '../compat/paths.cjs';
|
|
4
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
5
|
+
import { MAX_RESPONSE_BYTES } from './http.mjs';
|
|
6
|
+
|
|
7
|
+
function boundedText(value, bytes) {
|
|
8
|
+
if (typeof value !== 'string') return undefined;
|
|
9
|
+
const prefix = value.slice(0, bytes);
|
|
10
|
+
const buffer = Buffer.from(prefix);
|
|
11
|
+
if (buffer.length <= bytes) return buffer.toString('utf8');
|
|
12
|
+
let end = bytes;
|
|
13
|
+
while ((buffer[end] & 0xc0) === 0x80) end--;
|
|
14
|
+
return buffer.subarray(0, end).toString('utf8');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Private worker metadata only. Custom prototypes and arbitrary object graphs
|
|
18
|
+
// cannot cross processes; never attach this summary to an HTTP response.
|
|
19
|
+
export function summarizePageFailure(error) {
|
|
20
|
+
const read = name => { try { return error?.[name]; } catch { return undefined; } };
|
|
21
|
+
const statusCode = read('statusCode'), code = read('code'), stack = read('stack');
|
|
22
|
+
return { name: boundedText(read('name'), 128) ?? 'Error',
|
|
23
|
+
message: boundedText(typeof error === 'string' ? error : read('message'), 2048) ?? 'Page generation failed',
|
|
24
|
+
...(typeof stack === 'string' ? { stack: boundedText(stack, 4096) } : {}),
|
|
25
|
+
...(Number.isInteger(statusCode) && statusCode >= 100 && statusCode <= 599 ? { statusCode } : {}),
|
|
26
|
+
...(typeof code === 'string' ? { code: boundedText(code, 256) }
|
|
27
|
+
: code === null || typeof code === 'boolean' || typeof code === 'number' && Number.isFinite(code) ? { code } : {}) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function restorePageFailure(summary) {
|
|
31
|
+
if (!summary) return undefined;
|
|
32
|
+
const value = summarizePageFailure(summary);
|
|
33
|
+
return Object.assign(new Error(value.message), value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function builtinPageError(statusCode = 500, headers = {}) {
|
|
37
|
+
const title = statusCode === 404 ? 'This page could not be found.' : 'Internal Server Error';
|
|
38
|
+
return { status: statusCode, headers: { ...headers, 'content-type': 'text/html; charset=utf-8', 'cache-control': 'private, no-cache, no-store, max-age=0, must-revalidate' },
|
|
39
|
+
body: Buffer.from(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>${statusCode}: ${title}</title></head><body><main><h1>${statusCode}</h1><p>${title}</p></main></body></html>`) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function errorRoute(manifest = {}, statusCode, locale) {
|
|
43
|
+
const id = statusCode === 404 ? manifest.appNotFound || manifest.pagesErrors?.notFound || manifest.pagesErrors?.error
|
|
44
|
+
: manifest.pagesErrors?.serverError || manifest.pagesErrors?.error;
|
|
45
|
+
return manifest.routes?.find(route => route.id === (locale && locale !== manifest.config?.i18n?.defaultLocale && id !== manifest.appNotFound ? `${id}-locale-${locale}` : id));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function renderPageError(options, { statusCode = 500, error, responseHeaders = {} } = {}) {
|
|
49
|
+
const i18n = options.manifest?.config?.i18n;
|
|
50
|
+
const locale = options.route?.locale || (i18n && localePath(removeBasePath(new URL(options.url || 'http://prnext.local').pathname, options.manifest.config.basePath || ''), i18n).locale);
|
|
51
|
+
const route = errorRoute(options.manifest, statusCode, locale);
|
|
52
|
+
if (!route) return builtinPageError(statusCode, responseHeaders);
|
|
53
|
+
const seed = options.manifest?.prerendered?.find(page => page.path === route.pattern);
|
|
54
|
+
// Native owns the live 404/500 cache, including revalidation after build.
|
|
55
|
+
if (options.nativeErrors && (seed || route.router === 'app')) {
|
|
56
|
+
return { status: statusCode, pageError: statusCode, headers: { ...responseHeaders, 'cache-control': 'private, no-cache, no-store, max-age=0, must-revalidate' }, body: Buffer.alloc(0) };
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const distDir = options.distDir || path.dirname(path.dirname(options.modulePath));
|
|
60
|
+
if (seed && !options.manifest?.dev) {
|
|
61
|
+
const file = path.resolve(distDir, seed.file);
|
|
62
|
+
if (!file.startsWith(path.resolve(distDir) + path.sep) || (await stat(file)).size > MAX_RESPONSE_BYTES) throw new Error('Invalid prerendered error page');
|
|
63
|
+
return { status: statusCode, headers: { ...seed.headers, ...responseHeaders, 'content-type': 'text/html; charset=utf-8', 'cache-control': 'private, no-cache, no-store, max-age=0, must-revalidate' }, body: await readFile(file) };
|
|
64
|
+
}
|
|
65
|
+
const modulePath = path.resolve(distDir, route.module);
|
|
66
|
+
if (route.router === 'app') {
|
|
67
|
+
const { renderAppPage } = await import('./app-render.mjs');
|
|
68
|
+
const result = await renderAppPage({ ...options, modulePath, route, method: 'GET', body: '', stream: false });
|
|
69
|
+
return { ...result, status: 404 };
|
|
70
|
+
}
|
|
71
|
+
const { renderPage } = await import('./render.mjs');
|
|
72
|
+
responseHeaders = { ...responseHeaders };
|
|
73
|
+
for (const name of ['content-type', 'content-length', 'content-encoding', 'etag', 'last-modified']) delete responseHeaders[name];
|
|
74
|
+
const result = await renderPage({ ...options, route, modulePath, params: {}, staticProps: undefined,
|
|
75
|
+
...((route.originalPattern || route.pattern) !== '/_error' ? route.appGip && !route.ssg
|
|
76
|
+
? { url: `http://prnext.local${route.pattern}${new URL(options.originalUrl || options.url).search}`, originalUrl: options.originalUrl || options.url }
|
|
77
|
+
: { url: `http://prnext.local${route.pattern}`, originalUrl: undefined } : {}),
|
|
78
|
+
renderMode: 'error', errorDataRequest: options.renderMode === 'data', errorPage: true,
|
|
79
|
+
errorStatus: statusCode, error, responseHeaders, method: 'GET', body: '', isFallback: false });
|
|
80
|
+
return { ...result, headers: { ...result.headers, 'cache-control': 'private, no-cache, no-store, max-age=0, must-revalidate' } };
|
|
81
|
+
} catch (failure) {
|
|
82
|
+
console.error('[prnext] Error page failed:', failure?.stack || failure);
|
|
83
|
+
return builtinPageError(500, responseHeaders);
|
|
84
|
+
}
|
|
85
|
+
}
|