@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,130 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { setMaxListeners } from 'node:events';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { AppRouterProvider } from '../compat/app-context.cjs';
|
|
7
|
+
import { appContent } from './app-content.mjs';
|
|
8
|
+
import { runRequestContext, currentRequest } from '../compat/headers.cjs';
|
|
9
|
+
import { cachedValue, cacheGeneration, flushCacheWork } from '../compat/data-cache.cjs';
|
|
10
|
+
import { partialModel, preparePartialModel } from './app-partial-model.mjs';
|
|
11
|
+
import { MAX_RESPONSE_BYTES, escapeHtml } from './http.mjs';
|
|
12
|
+
import { fontPreloads } from './font-preload.mjs';
|
|
13
|
+
import { PartialArtifactCache } from './partial-artifact-cache.mjs';
|
|
14
|
+
|
|
15
|
+
const artifacts = new Map();
|
|
16
|
+
const parsedArtifacts = new PartialArtifactCache();
|
|
17
|
+
let artifactBytes = 0;
|
|
18
|
+
async function readArtifact(file) {
|
|
19
|
+
const hit = artifacts.get(file);
|
|
20
|
+
if (hit) { artifacts.delete(file); artifacts.set(file, hit); return hit.value; }
|
|
21
|
+
if ((await stat(file)).size > MAX_RESPONSE_BYTES * 2) throw new Error('Partial prerender artifact exceeds 32 MiB');
|
|
22
|
+
const source = await readFile(file, 'utf8');
|
|
23
|
+
const value = JSON.parse(source);
|
|
24
|
+
if (value.version !== 1 || typeof value.shell !== 'string' || typeof value.flight !== 'string' || !value.postponed) throw new Error('Invalid partial prerender artifact');
|
|
25
|
+
const size = Buffer.byteLength(source);
|
|
26
|
+
if (size <= 1024 * 1024) {
|
|
27
|
+
while (artifacts.size && (artifacts.size >= 32 || artifactBytes + size > 8 * 1024 * 1024)) {
|
|
28
|
+
const [key, old] = artifacts.entries().next().value;
|
|
29
|
+
artifacts.delete(key); artifactBytes -= old.size;
|
|
30
|
+
}
|
|
31
|
+
artifacts.set(file, { value, size }); artifactBytes += size;
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function bytes(stream) {
|
|
37
|
+
const chunks = [];
|
|
38
|
+
let length = 0;
|
|
39
|
+
for await (const chunk of stream) {
|
|
40
|
+
length += chunk.byteLength;
|
|
41
|
+
if (length > MAX_RESPONSE_BYTES) throw new Error('Partial prerender exceeds the 16 MiB response limit');
|
|
42
|
+
chunks.push(Buffer.from(chunk));
|
|
43
|
+
}
|
|
44
|
+
return Buffer.concat(chunks, length);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function prerenderPartialHtml(model, route, { strictMode = true, unknownParams = false, navigation = false, timeoutMs = 25_000 } = {}) {
|
|
48
|
+
const { prerender } = await import('react-dom/static');
|
|
49
|
+
const abort = new AbortController();
|
|
50
|
+
setMaxListeners(0, abort.signal);
|
|
51
|
+
const postponedError = new Error('PRNext request-dependent Suspense content');
|
|
52
|
+
const timer = setTimeout(() => abort.abort(new Error('Partial prerender timed out')), timeoutMs);
|
|
53
|
+
let scheduled = false, failure, clientDynamic = false;
|
|
54
|
+
const suspended = new Promise(() => {});
|
|
55
|
+
function onHole() {
|
|
56
|
+
if (!scheduled) {
|
|
57
|
+
scheduled = true;
|
|
58
|
+
// Let all ready static siblings and fallbacks commit before recording the
|
|
59
|
+
// opaque React continuation. No request work runs during this phase.
|
|
60
|
+
setImmediate(() => setImmediate(() => abort.abort(postponedError)));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
await preparePartialModel(model, { signal: abort.signal });
|
|
65
|
+
const tree = partialModel(model, { onHole, suspended });
|
|
66
|
+
const rendered = await prerender(React.createElement(strictMode ? React.StrictMode : React.Fragment, null,
|
|
67
|
+
React.createElement(AppRouterProvider, { router: tree.router, ...(!navigation ? { prerenderSearch() { clientDynamic = true; onHole(); throw suspended; } } : {}),
|
|
68
|
+
...(unknownParams ? { prerenderParams() { clientDynamic = true; onHole(); throw suspended; } } : {}) }, appContent(tree.tree))), {
|
|
69
|
+
signal: abort.signal,
|
|
70
|
+
onError(error) { if (error !== postponedError) failure ||= error; },
|
|
71
|
+
});
|
|
72
|
+
if (failure) throw failure;
|
|
73
|
+
let shell = (await bytes(rendered.prelude)).toString();
|
|
74
|
+
if (!/<html(?:\s|>)/i.test(shell) || !/<body(?:\s|>)/i.test(shell) || !/<\/head>/i.test(shell)) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const styles = fontPreloads(route.fonts) + (route.css || []).map(href => `<link rel="stylesheet" href="${escapeHtml(href)}">`).join('');
|
|
78
|
+
if (styles) shell = shell.replace(/<\/head>/i, `${styles}</head>`);
|
|
79
|
+
return { shell, postponed: rendered.postponed, clientDynamic };
|
|
80
|
+
} catch (error) {
|
|
81
|
+
// Request data above every Suspense boundary has no reusable document shell.
|
|
82
|
+
if (error === postponedError) return null;
|
|
83
|
+
throw error;
|
|
84
|
+
} finally { clearTimeout(timer); }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function partialArtifact({ request, route, manifest, filename, identity, regenerate, signal }) {
|
|
88
|
+
const root = path.resolve(request.distDir);
|
|
89
|
+
let build;
|
|
90
|
+
if (filename) {
|
|
91
|
+
const file = path.resolve(root, filename);
|
|
92
|
+
if (!file.startsWith(root + path.sep) || !filename.startsWith('static/')) throw new Error('Invalid partial prerender artifact path');
|
|
93
|
+
build = await readArtifact(file);
|
|
94
|
+
}
|
|
95
|
+
const external = build?.externalCache || Object.keys(manifest.config?.cacheHandlers || {}).length > 0;
|
|
96
|
+
// Rust resolves the generation and reads/acquires the lease atomically.
|
|
97
|
+
const key = createHash('sha256').update(`partial:${manifest.cacheId}:${route.id}:${identity || filename}`).digest('hex');
|
|
98
|
+
return runRequestContext({ ...request, signal }, async () => {
|
|
99
|
+
const context = currentRequest();
|
|
100
|
+
let metadata = build || { revalidate: route.cacheConfig?.revalidate ?? false, tags: [], paths: [] };
|
|
101
|
+
const producer = async ({ generation } = {}) => {
|
|
102
|
+
const lifetime = build?.revalidate === false || build?.revalidate == null ? Infinity : build.revalidate * 1000;
|
|
103
|
+
// Check after the native lease was acquired. An invalidation between the
|
|
104
|
+
// versioned read and producing the shell must not reseed old bytes.
|
|
105
|
+
if (build && !external && generation === 0 && Date.now() - build.generatedAt < lifetime && await cacheGeneration(signal) === 0) return Buffer.from(JSON.stringify(build));
|
|
106
|
+
const next = await regenerate(build);
|
|
107
|
+
if (next.dynamic) return Buffer.from(JSON.stringify({ dynamic: true }));
|
|
108
|
+
metadata = next.partial ? { ...next.partial, ...next.metadata }
|
|
109
|
+
: { version: 1, complete: true, shell: next.body, flight: next.flight.toString('base64'),
|
|
110
|
+
status: next.status, headers: next.headers, generatedAt: next.generatedAt,
|
|
111
|
+
revalidate: next.revalidate, tags: next.tags, paths: next.paths };
|
|
112
|
+
return Buffer.from(JSON.stringify(metadata));
|
|
113
|
+
};
|
|
114
|
+
try {
|
|
115
|
+
if (external || metadata.revalidate === 0) return JSON.parse((await producer()).toString());
|
|
116
|
+
const value = await cachedValue(key, producer, { context, signal, tags: metadata.tags, paths: metadata.paths,
|
|
117
|
+
revalidate: metadata.revalidate, resolveMetadata: () => ({ tags: metadata.tags, paths: metadata.paths,
|
|
118
|
+
revalidate: metadata.revalidate, expire: metadata.revalidate === false ? undefined : metadata.revalidate }), forceFresh: true, versioned: true });
|
|
119
|
+
return parsedArtifacts.parse(key, Buffer.isBuffer(value) ? value : Buffer.from(value));
|
|
120
|
+
} finally { await flushCacheWork(context); }
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function partialShell(artifact, route) {
|
|
125
|
+
const bootstrap = '<script>self.__PRNEXT_FLIGHT_STREAM__=self.__PRNEXT_FLIGHT_STREAM__||[];</script>' +
|
|
126
|
+
(route.client ? `<script type="module" async src="${escapeHtml(route.client)}"></script>` : '');
|
|
127
|
+
const end = artifact.shell.lastIndexOf('</body>');
|
|
128
|
+
if (end < 0 || !/<\/body><\/html>$/i.test(artifact.shell)) throw new Error('Invalid partial prerender document');
|
|
129
|
+
return artifact.shell.slice(0, end) + bootstrap + artifact.shell.slice(end);
|
|
130
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Prefetch contains only prerendered segments. Request-dependent holes are
|
|
2
|
+
// resolved by a fresh, authenticated navigation; they never enter this cache.
|
|
3
|
+
export const PARTIAL_PREFETCH_TYPE = 'application/x-prnext-ppr+json';
|
|
4
|
+
export const PARTIAL_PREFETCH_BYTES = 768 * 1024;
|
|
5
|
+
export const PARTIAL_PREFETCH_CACHE_BYTES = 2 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
export async function readPartialPrefetch(response) {
|
|
8
|
+
const reader = response.body.getReader();
|
|
9
|
+
const chunks = [];
|
|
10
|
+
let size = 0;
|
|
11
|
+
try {
|
|
12
|
+
for (;;) {
|
|
13
|
+
const { value, done } = await reader.read();
|
|
14
|
+
if (done) break;
|
|
15
|
+
size += value.byteLength;
|
|
16
|
+
if (size > PARTIAL_PREFETCH_BYTES) throw new Error('Partial prefetch exceeds the segment cache limit');
|
|
17
|
+
chunks.push(value);
|
|
18
|
+
}
|
|
19
|
+
} catch (error) { await reader.cancel(error).catch(() => {}); throw error; }
|
|
20
|
+
const bytes = new Uint8Array(size);
|
|
21
|
+
let offset = 0;
|
|
22
|
+
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
|
|
23
|
+
const value = JSON.parse(new TextDecoder().decode(bytes));
|
|
24
|
+
if (value?.version !== 1 || typeof value.id !== 'string' || value.id.length !== 64 ||
|
|
25
|
+
(value.flight !== undefined && typeof value.flight !== 'string') || !Array.isArray(value.keys) ||
|
|
26
|
+
typeof value.router?.pathname !== 'string') throw new Error('Invalid partial prefetch response');
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function partialFlightStream(base64) {
|
|
31
|
+
const binary = atob(base64);
|
|
32
|
+
const bytes = Uint8Array.from(binary, character => character.charCodeAt(0));
|
|
33
|
+
return new ReadableStream({ start(controller) { controller.enqueue(bytes); controller.close(); } });
|
|
34
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { renderToReadableStream } from 'react-dom/server';
|
|
3
|
+
import { MAX_RESPONSE_BYTES } from './http.mjs';
|
|
4
|
+
import { appTimeoutError } from './app-errors.mjs';
|
|
5
|
+
|
|
6
|
+
const charset = '<meta charSet="utf-8"/>';
|
|
7
|
+
const viewport = '<meta name="viewport" content="width=device-width, initial-scale=1"/>';
|
|
8
|
+
const robots = '<meta name="robots" content="noindex"/>';
|
|
9
|
+
const envelope = head => `<!DOCTYPE html><html id="__prnext_error__"><head>${head}</head><body></body></html>`;
|
|
10
|
+
const defaults = envelope(charset + viewport + robots);
|
|
11
|
+
|
|
12
|
+
function recoveryHead(markup) {
|
|
13
|
+
let hasCharset = false, hasViewport = false;
|
|
14
|
+
const head = markup.replace(/<meta\b[^>]*>/gi, tag => {
|
|
15
|
+
const attributes = Object.create(null);
|
|
16
|
+
for (const match of tag.slice(5, -1).matchAll(/([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g)) {
|
|
17
|
+
attributes[match[1].toLowerCase()] = match[2] ?? match[3] ?? match[4] ?? '';
|
|
18
|
+
}
|
|
19
|
+
hasCharset ||= Object.hasOwn(attributes, 'charset');
|
|
20
|
+
hasViewport ||= attributes.name?.toLowerCase() === 'viewport';
|
|
21
|
+
// Failed documents must remain noindex even if the route requested index.
|
|
22
|
+
return attributes.name?.toLowerCase() === 'robots' ? '' : tag;
|
|
23
|
+
});
|
|
24
|
+
return (hasCharset ? '' : charset) + (hasViewport ? '' : viewport) + head + robots;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Render the already-decoded metadata, never the failed page or its layout. */
|
|
28
|
+
export async function renderAppRecoveryShell(head, { signal, timeoutMs = 25_000 } = {}) {
|
|
29
|
+
signal?.throwIfAborted();
|
|
30
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) throw new TypeError('Recovery timeout must be a nonnegative number');
|
|
31
|
+
if (head == null) return defaults;
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
const combined = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
|
34
|
+
let reader, metadataError, rejectAbort;
|
|
35
|
+
const aborted = new Promise((_, reject) => { rejectAbort = reject; });
|
|
36
|
+
const onAbort = () => rejectAbort(combined.reason);
|
|
37
|
+
combined.addEventListener('abort', onAbort, { once: true });
|
|
38
|
+
const timer = setTimeout(() => controller.abort(appTimeoutError('recovery metadata', timeoutMs)), timeoutMs);
|
|
39
|
+
try {
|
|
40
|
+
const tree = React.createElement('html', { id: '__prnext_error__' },
|
|
41
|
+
React.createElement('head', null, head), React.createElement('body'));
|
|
42
|
+
const stream = await Promise.race([renderToReadableStream(tree, {
|
|
43
|
+
signal: combined,
|
|
44
|
+
onError(error) { metadataError ||= error || new Error('Metadata rendering failed'); },
|
|
45
|
+
}), aborted]);
|
|
46
|
+
// Waiting for all metadata before pulling lets React hoist suspended title
|
|
47
|
+
// and meta elements into the head, without publishing a partial shell.
|
|
48
|
+
await Promise.race([stream.allReady, aborted]);
|
|
49
|
+
combined.throwIfAborted();
|
|
50
|
+
if (metadataError) throw metadataError;
|
|
51
|
+
reader = stream.getReader();
|
|
52
|
+
const chunks = [];
|
|
53
|
+
let length = 0;
|
|
54
|
+
for (;;) {
|
|
55
|
+
const { value, done } = await Promise.race([reader.read(), aborted]);
|
|
56
|
+
if (done) break;
|
|
57
|
+
length += value.byteLength;
|
|
58
|
+
if (length > MAX_RESPONSE_BYTES) {
|
|
59
|
+
throw Object.assign(new Error('Recovery HTML exceeds the 16 MiB PRNext limit'), { code: 'PRNEXT_RECOVERY_HTML_TOO_LARGE' });
|
|
60
|
+
}
|
|
61
|
+
chunks.push(Buffer.from(value));
|
|
62
|
+
}
|
|
63
|
+
combined.throwIfAborted();
|
|
64
|
+
if (metadataError) throw metadataError;
|
|
65
|
+
const rendered = Buffer.concat(chunks, length).toString('utf8');
|
|
66
|
+
const metadata = /<head>([\s\S]*?)<\/head>/i.exec(rendered)?.[1];
|
|
67
|
+
if (metadata === undefined) throw new Error('Metadata renderer did not produce a head');
|
|
68
|
+
const html = envelope(recoveryHead(metadata));
|
|
69
|
+
if (Buffer.byteLength(html) > MAX_RESPONSE_BYTES) {
|
|
70
|
+
throw Object.assign(new Error('Recovery HTML exceeds the 16 MiB PRNext limit'), { code: 'PRNEXT_RECOVERY_HTML_TOO_LARGE' });
|
|
71
|
+
}
|
|
72
|
+
return html;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (combined.aborted) throw combined.reason;
|
|
75
|
+
if (error?.code === 'PRNEXT_RECOVERY_HTML_TOO_LARGE') throw error;
|
|
76
|
+
// A rejected metadata Flight element must not trigger another route render.
|
|
77
|
+
return defaults;
|
|
78
|
+
} finally {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
combined.removeEventListener('abort', onAbort);
|
|
81
|
+
controller.abort();
|
|
82
|
+
if (reader) {
|
|
83
|
+
try { Promise.resolve(reader.cancel()).catch(() => {}); } catch {}
|
|
84
|
+
try { reader.releaseLock(); } catch {}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|