@thomas.f/prnext 0.1.17-alpha → 0.1.19-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 +27 -868
- package/build/actions.mjs +27 -15
- package/build/analyze.mjs +180 -24
- package/build/app-config.mjs +13 -0
- package/build/app-static.mjs +34 -12
- package/build/app-styles.mjs +79 -0
- package/build/app.mjs +122 -46
- package/build/bundle-runtime.mjs +22 -9
- package/build/cache-components.mjs +1 -1
- package/build/cache-handlers.mjs +1 -1
- package/build/compact-css.mjs +53 -0
- package/build/compilation-cache.mjs +269 -0
- package/build/compilation-store.mjs +142 -0
- package/build/compiler.mjs +34 -10
- package/build/config.mjs +34 -3
- package/build/create.mjs +239 -0
- package/build/css-modules.mjs +56 -12
- package/build/deployment-assets.mjs +89 -0
- package/build/deployment-config.mjs +18 -0
- package/build/dev-css-assets.mjs +173 -0
- package/build/dev-css-update.mjs +18 -14
- package/build/dev-dependencies.mjs +1 -1
- package/build/dev-discovery.mjs +3 -3
- package/build/dev-lazy-build.mjs +77 -0
- package/build/dev.mjs +15 -8
- package/build/dynamic.mjs +14 -7
- package/build/edge.mjs +3 -3
- package/build/fonts.mjs +1 -1
- package/build/framework-imports.mjs +15 -5
- package/build/html-only-config.mjs +53 -0
- package/build/html-only-graph.mjs +67 -0
- package/build/incremental.mjs +112 -18
- package/build/index.mjs +90 -28
- package/build/instrumentation.mjs +30 -0
- package/build/lazy-dynamic-imports.mjs +137 -0
- package/build/metadata-routes.mjs +1 -1
- package/build/migrate.mjs +4 -4
- package/build/migration-audit.mjs +19 -31
- package/build/migration-imports.mjs +245 -0
- package/build/migration-scenarios-runner.mjs +73 -0
- package/build/migration-scenarios.mjs +72 -0
- package/build/native-discovery.mjs +65 -5
- package/build/native-packages.mjs +2 -0
- package/build/native-type-config.mjs +37 -0
- package/build/npm-packages.mjs +1 -0
- package/build/persistent-cache.mjs +18 -3
- package/build/precompress.mjs +1 -1
- package/build/prerender-cache.mjs +70 -0
- package/build/prerender-config.mjs +29 -0
- package/build/prerender-pages.mjs +19 -9
- package/build/prerender-pool.mjs +165 -0
- package/build/prerender-process-worker.mjs +58 -0
- package/build/prerender-worker.mjs +5 -5
- package/build/production-builder.mjs +59 -10
- package/build/react-compiler-report.mjs +225 -0
- package/build/react-compiler.mjs +139 -0
- package/build/runtime-contract.mjs +2 -2
- package/build/standalone.mjs +21 -3
- package/build/static-export.mjs +4 -4
- package/build/styles.mjs +6 -4
- package/build/transform.mjs +1 -1
- package/build/typed-routes.mjs +4 -0
- package/build/typescript-dev.mjs +35 -8
- package/build/typescript-native-worker.mjs +55 -0
- package/build/typescript-native.mjs +18 -0
- package/build/typescript-toolchain.mjs +19 -0
- package/build/typescript.mjs +6 -4
- package/build/webpack-graph.mjs +19 -6
- package/build/worker-memory.mjs +9 -0
- package/cli.mjs +70 -5
- package/compat/activity-cache.cjs +26 -0
- package/compat/after.cjs +6 -4
- package/compat/app-dynamic.cjs +12 -1
- package/compat/app-layout-context.cjs +52 -2
- package/compat/base64.cjs +10 -0
- package/compat/cache-handlers.cjs +15 -3
- package/compat/cache-wire.cjs +28 -0
- package/compat/cache.cjs +2 -1
- package/compat/cache.d.cts +4 -0
- package/compat/cookies.cjs +21 -5
- package/compat/data-cache.cjs +12 -8
- package/compat/flight-delta.cjs +152 -0
- package/compat/headers.cjs +4 -2
- package/compat/incremental-cache.cjs +20 -7
- package/compat/index.d.ts +69 -4
- package/compat/link.cjs +13 -10
- package/compat/navigation-authorization.cjs +9 -0
- package/compat/navigation-stages.cjs +19 -0
- package/compat/offline-store.cjs +11 -0
- package/compat/offline.cjs +5 -0
- package/compat/offline.d.cts +2 -0
- package/compat/redis-cache.cjs +201 -0
- package/compat/redis-cache.d.cts +37 -0
- package/compat/server-primitives.cjs +31 -11
- package/compat/server.d.cts +15 -2
- package/compat/static-generation.cjs +7 -0
- package/compat/use-cache.cjs +3 -0
- package/docs/ai/compatibility.md +248 -2
- package/docs/ai/data.md +165 -5
- package/docs/ai/deployment.md +158 -0
- package/docs/ai/quickstart.md +29 -4
- package/docs/ai/routing.md +93 -0
- package/docs/ai/troubleshooting.md +116 -2
- package/package.json +40 -9
- package/runtime/action-client.mjs +10 -4
- package/runtime/action-server.mjs +2 -0
- package/runtime/app-client.mjs +316 -92
- package/runtime/app-content.mjs +4 -3
- package/runtime/app-partial-client.mjs +3 -0
- package/runtime/app-partial.mjs +1 -1
- package/runtime/app-prefetch-constants.mjs +4 -0
- package/runtime/app-prefetch.mjs +2 -3
- package/runtime/app-render-key.mjs +5 -0
- package/runtime/app-render.mjs +88 -28
- package/runtime/app-routing.mjs +22 -9
- package/runtime/app-static.mjs +18 -8
- package/runtime/app-stream-html.mjs +10 -53
- package/runtime/app-style-selection.mjs +34 -0
- package/runtime/app-styles.mjs +154 -0
- package/runtime/atomic-file.mjs +27 -0
- package/runtime/cache-components-rsc.mjs +7 -2
- package/runtime/cache-request.mjs +2 -1
- package/runtime/deployment-client.mjs +19 -0
- package/runtime/dev-client.mjs +13 -3
- package/runtime/dev-compiler-broker.mjs +42 -0
- package/runtime/dev-file-changes.mjs +123 -8
- package/runtime/dev-indicator.mjs +8 -3
- package/runtime/dev-lazy-imports.mjs +121 -0
- package/runtime/dev-lazy-staging.mjs +14 -0
- package/runtime/dev-lazy-tool.mjs +16 -0
- package/runtime/dev-style-retention.mjs +15 -0
- package/runtime/development.mjs +83 -25
- package/runtime/document.mjs +1 -1
- package/runtime/edge.mjs +6 -5
- package/runtime/fetch-cache.mjs +10 -8
- package/runtime/flight-delta.mjs +74 -0
- package/runtime/flight-tap.mjs +28 -0
- package/runtime/fs.mjs +15 -8
- package/runtime/html-only.mjs +27 -0
- package/runtime/http.mjs +2 -0
- package/runtime/incremental-cache.mjs +25 -4
- package/runtime/instrumentation.mjs +49 -0
- package/runtime/lazy-import-client.mjs +38 -0
- package/runtime/navigation-recovery.mjs +36 -0
- package/runtime/navigation.mjs +7 -0
- package/runtime/node-html-stream.mjs +47 -0
- package/runtime/pages-client.mjs +14 -7
- package/runtime/pages-navigation.mjs +48 -63
- package/runtime/prefetch-scheduler.mjs +110 -0
- package/runtime/profile.mjs +14 -0
- package/runtime/profiles.json +2 -2
- package/runtime/render.mjs +3 -0
- package/runtime/route-pattern.mjs +44 -0
- package/runtime/rsc-worker.mjs +34 -12
- package/runtime/trace.mjs +78 -0
- package/runtime/watch-project.mjs +19 -15
- package/runtime/worker.mjs +70 -26
- package/vendor/nft/LICENSE +7 -0
- package/vendor/nft/README.md +16 -0
- package/vendor/nft/out/analyze.d.ts +8 -0
- package/vendor/nft/out/analyze.js +1216 -0
- package/vendor/nft/out/analyze.js.map +1 -0
- package/vendor/nft/out/cli.d.ts +2 -0
- package/vendor/nft/out/cli.js +94 -0
- package/vendor/nft/out/cli.js.map +1 -0
- package/vendor/nft/out/fs.d.ts +22 -0
- package/vendor/nft/out/fs.js +106 -0
- package/vendor/nft/out/fs.js.map +1 -0
- package/vendor/nft/out/index.d.ts +4 -0
- package/vendor/nft/out/index.js +25 -0
- package/vendor/nft/out/index.js.map +1 -0
- package/vendor/nft/out/node-file-trace.d.ts +41 -0
- package/vendor/nft/out/node-file-trace.js +380 -0
- package/vendor/nft/out/node-file-trace.js.map +1 -0
- package/vendor/nft/out/resolve-dependency.d.ts +6 -0
- package/vendor/nft/out/resolve-dependency.js +427 -0
- package/vendor/nft/out/resolve-dependency.js.map +1 -0
- package/vendor/nft/out/types.d.ts +66 -0
- package/vendor/nft/out/types.js +2 -0
- package/vendor/nft/out/types.js.map +1 -0
- package/vendor/nft/out/utils/ast-helpers.js +49 -0
- package/vendor/nft/out/utils/ast-helpers.js.map +1 -0
- package/vendor/nft/out/utils/binary-locators.js +88 -0
- package/vendor/nft/out/utils/binary-locators.js.map +1 -0
- package/vendor/nft/out/utils/get-package-base.js +30 -0
- package/vendor/nft/out/utils/get-package-base.js.map +1 -0
- package/vendor/nft/out/utils/interop-require.js +24 -0
- package/vendor/nft/out/utils/interop-require.js.map +1 -0
- package/vendor/nft/out/utils/node-version.js +10 -0
- package/vendor/nft/out/utils/node-version.js.map +1 -0
- package/vendor/nft/out/utils/sharedlib-emit.js +33 -0
- package/vendor/nft/out/utils/sharedlib-emit.js.map +1 -0
- package/vendor/nft/out/utils/special-cases.js +347 -0
- package/vendor/nft/out/utils/special-cases.js.map +1 -0
- package/vendor/nft/out/utils/static-eval.js +579 -0
- package/vendor/nft/out/utils/static-eval.js.map +1 -0
- package/vendor/nft/out/utils/types.js +2 -0
- package/vendor/nft/out/utils/types.js.map +1 -0
- package/vendor/nft/out/utils/wrappers.js +659 -0
- package/vendor/nft/out/utils/wrappers.js.map +1 -0
- package/vendor/nft/package.json +4 -0
- package/vendor/nft/upstream.json +51 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {reportRequestError,acknowledgeRequestError} from './instrumentation.mjs';
|
|
1
2
|
import { htmlEnvelope } from './html-envelope.mjs';
|
|
2
3
|
import React from 'react';
|
|
3
4
|
import { fontPreloads } from './font-preload.mjs';
|
|
@@ -12,55 +13,9 @@ import { addBasePath } from '../compat/paths.cjs';
|
|
|
12
13
|
import { renderAppRecoveryShell } from './app-recovery.mjs';
|
|
13
14
|
import { partialModel } from './app-partial-model.mjs';
|
|
14
15
|
import { streamSelector } from './stream-select.mjs';
|
|
16
|
+
import {nodeHtmlReader,useNodeHtml} from './node-html-stream.mjs';
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
// ReadableStream.tee(), this tap stops reading when the HTML consumer stalls.
|
|
18
|
-
// Before a document shell exists, at most the overall 16 MiB response limit can
|
|
19
|
-
// accumulate: an async root layout may need the complete model to produce it.
|
|
20
|
-
function flightTap(source) {
|
|
21
|
-
const reader = source.getReader();
|
|
22
|
-
const queue = [];
|
|
23
|
-
let bytes = 0;
|
|
24
|
-
let done = false;
|
|
25
|
-
let failure;
|
|
26
|
-
let outputStarted = false;
|
|
27
|
-
let wakeData;
|
|
28
|
-
let wakeSpace;
|
|
29
|
-
const finish = error => {
|
|
30
|
-
done = true; failure = error;
|
|
31
|
-
wakeData?.(); wakeData = undefined;
|
|
32
|
-
wakeSpace?.(); wakeSpace = undefined;
|
|
33
|
-
};
|
|
34
|
-
const cancel = async reason => { finish(reason); await reader.cancel(reason).catch(() => {}); };
|
|
35
|
-
const body = new ReadableStream({
|
|
36
|
-
async pull(controller) {
|
|
37
|
-
try {
|
|
38
|
-
while (!done && outputStarted && bytes >= 128 * 1024) await new Promise(resolve => { wakeSpace = resolve; });
|
|
39
|
-
if (done) { if (failure) controller.error(failure); else controller.close(); return; }
|
|
40
|
-
const next = await reader.read();
|
|
41
|
-
if (next.done) { finish(); controller.close(); return; }
|
|
42
|
-
queue.push(next.value); bytes += next.value.byteLength;
|
|
43
|
-
wakeData?.(); wakeData = undefined;
|
|
44
|
-
controller.enqueue(next.value);
|
|
45
|
-
} catch (error) { finish(error); controller.error(error); }
|
|
46
|
-
},
|
|
47
|
-
cancel,
|
|
48
|
-
}, { highWaterMark: 0 });
|
|
49
|
-
return {
|
|
50
|
-
body, cancel,
|
|
51
|
-
startOutput() { outputStarted = true; },
|
|
52
|
-
async next() {
|
|
53
|
-
while (!queue.length && !done) await new Promise(resolve => { wakeData = resolve; });
|
|
54
|
-
if (queue.length) {
|
|
55
|
-
const value = queue.shift(); bytes -= value.byteLength;
|
|
56
|
-
wakeSpace?.(); wakeSpace = undefined;
|
|
57
|
-
return { value, done: false };
|
|
58
|
-
}
|
|
59
|
-
if (failure) throw failure;
|
|
60
|
-
return { done: true };
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
}
|
|
18
|
+
import {flightTap} from './flight-tap.mjs';
|
|
64
19
|
|
|
65
20
|
export async function renderProgressiveAppHtml({ result, request, route, responseHeaders, decodeFlight, signal, timeoutMs, partial }) {
|
|
66
21
|
timeoutMs ??= route?.cacheConfig?.maxDuration ? route.cacheConfig.maxDuration * 1000 : 25_000;
|
|
@@ -92,6 +47,7 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
|
|
|
92
47
|
onError(error) {
|
|
93
48
|
if (isDynamicBailout(error)) return error.digest;
|
|
94
49
|
if (combinedSignal.aborted) return error?.digest;
|
|
50
|
+
if(result.reportedDigests?.has(error?.digest))acknowledgeRequestError(error);else reportRequestError(error);
|
|
95
51
|
const navigation = navigationResponse(error);
|
|
96
52
|
if (navigation) {
|
|
97
53
|
if (committed) control = navigation;
|
|
@@ -104,8 +60,9 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
|
|
|
104
60
|
const element = React.createElement(request.strictMode !== false ? React.StrictMode : React.Fragment, null, React.createElement(AppRouterProvider, { router: resumedModel.router, nonce: request.nonce }, appContent(resumedModel.tree)));
|
|
105
61
|
// React owns and mutates its opaque continuation during resume. Parsed
|
|
106
62
|
// artifacts can be shared, but each HTML request needs its own continuation.
|
|
107
|
-
|
|
108
|
-
|
|
63
|
+
reader = !partial&&request.cacheConfig?.runtime!=='edge'&&useNodeHtml()
|
|
64
|
+
? await nodeHtmlReader(element,renderOptions)
|
|
65
|
+
: (partial ? await resume(element, structuredClone(partial.postponed), renderOptions) : await renderToReadableStream(element, renderOptions)).getReader();
|
|
109
66
|
const decoder = new TextDecoder();
|
|
110
67
|
let prefix = partial?.shell || '';
|
|
111
68
|
let htmlDone = false;
|
|
@@ -116,13 +73,13 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
|
|
|
116
73
|
const next = await reader.read();
|
|
117
74
|
if (next.done) { htmlDone = true; prefix += decoder.decode(); break; }
|
|
118
75
|
prefix += decoder.decode(next.value, { stream: true });
|
|
119
|
-
if (Buffer.byteLength(prefix) > MAX_RESPONSE_BYTES) throw new Error('HTML shell exceeds the 16 MiB PRNext limit');
|
|
76
|
+
if (Buffer.byteLength(prefix) > MAX_RESPONSE_BYTES) throw Object.assign(new Error('HTML shell exceeds the 16 MiB PRNext limit'),{code:'PRNEXT_RESPONSE_TOO_LARGE'});
|
|
120
77
|
}
|
|
121
78
|
if (renderError) throw renderError;
|
|
122
79
|
if (!/<html(?:\s|>)/i.test(prefix) || !/<body(?:\s|>)/i.test(prefix) || !/<\/head>/i.test(prefix)) {
|
|
123
80
|
throw Object.assign(new Error('App Router root layout must render <html>, <head> and <body> elements'), { code: 'PRNEXT_INVALID_ROOT_LAYOUT' });
|
|
124
81
|
}
|
|
125
|
-
const styles = fontPreloads(route.fonts, request.nonce) + (route.css
|
|
82
|
+
const styles = fontPreloads(route.fonts, request.nonce) + (model.css ?? route.css ?? []).map(href => `<link rel="stylesheet" href="${escapeHtml(href)}">`).join('');
|
|
126
83
|
if (styles && !partial) prefix = prefix.replace(/<\/head>/i, `${styles}</head>`);
|
|
127
84
|
tap.startOutput();
|
|
128
85
|
committed = true;
|
|
@@ -183,7 +140,7 @@ export async function renderProgressiveAppHtml({ result, request, route, respons
|
|
|
183
140
|
return { status, headers: { ...responseHeaders, ...result.headers, 'content-type': 'text/html; charset=utf-8' }, body, cancel };
|
|
184
141
|
} catch (error) {
|
|
185
142
|
if (!committed && !combinedSignal.aborted && error?.statusCode !== 504 &&
|
|
186
|
-
error?.code !== 'PRNEXT_INVALID_ROOT_LAYOUT' && !isDynamicBailout(error) && (!navigationResponse(error) || (route.parallel && navigationResponse(error).status === 404))) {
|
|
143
|
+
error?.code !== 'PRNEXT_INVALID_ROOT_LAYOUT' && error?.code !== 'PRNEXT_RESPONSE_TOO_LARGE' && !isDynamicBailout(error) && (!navigationResponse(error) || (route.parallel && navigationResponse(error).status === 404))) {
|
|
187
144
|
try {
|
|
188
145
|
// Next emits an empty document after a shell failure. The original
|
|
189
146
|
// Flight stream still carries the failing subtree to client boundaries;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function ownsBranch(scope, branch) {
|
|
2
|
+
const path = branch.replace(/#default$/, '');
|
|
3
|
+
if (scope.kind === 'layout') return !scope.path || path === scope.path || path.startsWith(scope.path + '/');
|
|
4
|
+
return path === scope.path && (scope.kind === 'default') === branch.endsWith('#default');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Parallel route entries include both canonical and intercepted alternatives.
|
|
8
|
+
* Their actual router selection determines which sheets may become active. */
|
|
9
|
+
export function selectAppStyles(model) {
|
|
10
|
+
if (!model.routing || !Array.isArray(model.cssScopes)) return;
|
|
11
|
+
const branches = Object.values(model.routing.slots || {}).map(slot => slot.branch);
|
|
12
|
+
model.cssScopes = model.cssScopes.filter(scope => (scope.kind === 'layout' && !scope.path) || branches.some(branch => ownsBranch(scope, branch)));
|
|
13
|
+
model.css = [...new Set(model.cssScopes.flatMap(scope => scope.css || []))];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A retained parallel/intercepted slot keeps only the styles belonging to
|
|
17
|
+
* that branch. Carrying the preceding route's complete list would leak its
|
|
18
|
+
* inactive page globals into the new main page. */
|
|
19
|
+
export function retainAppStyles(model, previous) {
|
|
20
|
+
selectAppStyles(model);
|
|
21
|
+
if (!model.routing || !previous?.routing || model.globalError) return;
|
|
22
|
+
const retained = Object.entries(model.routing.slots || {}).flatMap(([key, slot]) => {
|
|
23
|
+
const old = previous.routing.slots?.[key];
|
|
24
|
+
return old && old.branch === slot.branch && old.url === slot.url ? [slot.branch] : [];
|
|
25
|
+
});
|
|
26
|
+
if (!retained.length) return;
|
|
27
|
+
const scopes = [...model.cssScopes || []];
|
|
28
|
+
for (const scope of previous.cssScopes || []) {
|
|
29
|
+
if (scopes.some(current => current.path === scope.path && current.kind === scope.kind)) continue;
|
|
30
|
+
if (retained.some(branch => ownsBranch(scope, branch))) scopes.push(scope);
|
|
31
|
+
}
|
|
32
|
+
model.cssScopes = scopes;
|
|
33
|
+
model.css = [...new Set([...(model.css || []), ...scopes.flatMap(scope => scope.css || [])])];
|
|
34
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
const managers = new WeakMap();
|
|
2
|
+
|
|
3
|
+
function aborted() { return new DOMException('Navigation cancelled', 'AbortError'); }
|
|
4
|
+
|
|
5
|
+
/** Styles are downloaded while the old page remains visible, but become
|
|
6
|
+
* active together in the new tree's layout effect, before the next paint.
|
|
7
|
+
* Keeping one manager per document also covers development runtime updates. */
|
|
8
|
+
export function appStyleManager(document, { timeout = 15_000 } = {}) {
|
|
9
|
+
if (managers.has(document)) return managers.get(document);
|
|
10
|
+
const sheets = new Map();
|
|
11
|
+
let handed = new Map();
|
|
12
|
+
let cleanup = 0;
|
|
13
|
+
const reserved = sheet => handed.get(sheet.link.href) === sheet;
|
|
14
|
+
function urls(values) {
|
|
15
|
+
if (!Array.isArray(values) || values.length > 1024) throw new Error('Invalid App Router stylesheet list');
|
|
16
|
+
return [...new Set(values.map(value => {
|
|
17
|
+
if (typeof value !== 'string' || value.length > 4096) throw new Error('Invalid App Router stylesheet URL');
|
|
18
|
+
const url = new URL(value, document.baseURI);
|
|
19
|
+
if (!['http:', 'https:'].includes(url.protocol)) throw new Error('Invalid App Router stylesheet protocol');
|
|
20
|
+
return url.href;
|
|
21
|
+
}))];
|
|
22
|
+
}
|
|
23
|
+
function remove(href, sheet) {
|
|
24
|
+
if (sheet.removed) return;
|
|
25
|
+
sheet.removed = true;
|
|
26
|
+
sheet.detach?.();
|
|
27
|
+
sheet.link.remove();
|
|
28
|
+
const records = sheets.get(href);
|
|
29
|
+
if (records) {
|
|
30
|
+
const index = records.indexOf(sheet);
|
|
31
|
+
if (index >= 0) records.splice(index, 1);
|
|
32
|
+
if (!records.length) sheets.delete(href);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function acquire(href, before, replace = false) {
|
|
36
|
+
const records = sheets.get(href) || [];
|
|
37
|
+
const current = records.at(-1);
|
|
38
|
+
if (current && !replace) return current;
|
|
39
|
+
// A replacement keeps the old active (or local-reset recovery) occurrence
|
|
40
|
+
// in place until commit. Discard older abandoned candidates immediately.
|
|
41
|
+
if (replace) for (const record of [...records]) if (!record.active && !record.recovery && !reserved(record)) remove(href, record);
|
|
42
|
+
const tracked = new Set(records.map(record => record.link));
|
|
43
|
+
const existing = replace ? undefined : [...document.querySelectorAll('link[rel="stylesheet"]')].find(link => link.href === href && !tracked.has(link));
|
|
44
|
+
const link = existing || document.createElement('link');
|
|
45
|
+
const sheet = { link, active: !!existing, ready: !!existing?.sheet, recovery: false };
|
|
46
|
+
const retained = sheets.get(href) || [];
|
|
47
|
+
retained.push(sheet); sheets.set(href, retained);
|
|
48
|
+
if (sheet.ready) sheet.promise = Promise.resolve();
|
|
49
|
+
else sheet.promise = new Promise((resolve, reject) => {
|
|
50
|
+
let finished = false;
|
|
51
|
+
const detach = () => { clearTimeout(timer); link.removeEventListener('load', loaded); link.removeEventListener('error', failed); };
|
|
52
|
+
const finish = error => {
|
|
53
|
+
if (finished) return;
|
|
54
|
+
finished = true; detach(); sheet.detach = undefined;
|
|
55
|
+
if (error) { remove(href, sheet); reject(error); }
|
|
56
|
+
else { sheet.ready = true; resolve(); }
|
|
57
|
+
};
|
|
58
|
+
const loaded = () => finish();
|
|
59
|
+
const failed = () => finish(new Error(`Could not load stylesheet ${href}`));
|
|
60
|
+
const timer = setTimeout(() => finish(new Error('App Router stylesheet loading timed out')), timeout);
|
|
61
|
+
sheet.detach = () => finish(aborted());
|
|
62
|
+
link.addEventListener('load', loaded);
|
|
63
|
+
link.addEventListener('error', failed);
|
|
64
|
+
});
|
|
65
|
+
// Cancellation and a competing navigation can remove a pending sheet.
|
|
66
|
+
// Each caller still observes the original rejection through prepare().
|
|
67
|
+
void sheet.promise.catch(() => {});
|
|
68
|
+
if (!existing) {
|
|
69
|
+
link.rel = 'stylesheet'; link.href = href; link.media = 'not all';
|
|
70
|
+
link.dataset.prnextAppStyle = '';
|
|
71
|
+
if (before) document.head.insertBefore(link, before);
|
|
72
|
+
else document.head.append(link);
|
|
73
|
+
}
|
|
74
|
+
return sheet;
|
|
75
|
+
}
|
|
76
|
+
const manager = {
|
|
77
|
+
async prepare(values = [], signal) {
|
|
78
|
+
if (signal?.aborted) throw aborted();
|
|
79
|
+
const wanted = urls(values);
|
|
80
|
+
const prepared = [];
|
|
81
|
+
let before;
|
|
82
|
+
// Establish cascade order before downloading. Re-inserting an already
|
|
83
|
+
// loaded link can unload its CSSOM and refetch, even within the same head.
|
|
84
|
+
// Keep a correctly ordered suffix; prepare a separate inactive occurrence
|
|
85
|
+
// when an existing sheet would otherwise need to move across that suffix.
|
|
86
|
+
for (let index = wanted.length - 1; index >= 0; index--) {
|
|
87
|
+
const href = wanted[index];
|
|
88
|
+
let sheet = acquire(href, before);
|
|
89
|
+
if (before && !(sheet.link.compareDocumentPosition(before) & 4)) sheet = acquire(href, before, true);
|
|
90
|
+
prepared.push(sheet.promise);
|
|
91
|
+
before = sheet.link;
|
|
92
|
+
}
|
|
93
|
+
const pending = Promise.all(prepared);
|
|
94
|
+
if (!signal) return pending;
|
|
95
|
+
await new Promise((resolve, reject) => {
|
|
96
|
+
const cancel = () => reject(aborted());
|
|
97
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
98
|
+
pending.then(resolve, reject).finally(() => signal.removeEventListener('abort', cancel));
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
handoff(values = []) {
|
|
102
|
+
// A concurrent React render can suspend after its CSS has loaded. Pin
|
|
103
|
+
// these exact occurrences until that render commits or is superseded;
|
|
104
|
+
// pagehide/actions must cancel network work, not its already-ready CSS.
|
|
105
|
+
const next = new Map(urls(values).map(href => {
|
|
106
|
+
const sheet = sheets.get(href)?.at(-1);
|
|
107
|
+
if (!sheet?.ready) throw new Error('App Router styles were not prepared before rendering');
|
|
108
|
+
return [href, sheet];
|
|
109
|
+
}));
|
|
110
|
+
const previous = handed; handed = next;
|
|
111
|
+
for (const [href, sheet] of previous) if (!reserved(sheet) && !sheet.active && !sheet.recovery) remove(href, sheet);
|
|
112
|
+
},
|
|
113
|
+
commit(values = []) {
|
|
114
|
+
cleanup++;
|
|
115
|
+
const wanted = new Set(urls(values));
|
|
116
|
+
const handedUrls = [...handed.keys()];
|
|
117
|
+
const pinned = wanted.size === handed.size && [...wanted].every((href, index) => handedUrls[index] === href) ? handed : null;
|
|
118
|
+
for (const href of wanted) {
|
|
119
|
+
const records = sheets.get(href) || [];
|
|
120
|
+
const prior = !pinned && handed.size ? records.find(record => record.active) || records.find(record => record.recovery) : undefined;
|
|
121
|
+
const sheet = pinned?.get(href) || prior || records.findLast(record => record.ready) || acquire(href);
|
|
122
|
+
// Initial HTML links are render-blocking. A missing sheet is restored
|
|
123
|
+
// on explicit recovery; normal navigation always prepares first.
|
|
124
|
+
sheet.active = true; sheet.recovery = false; sheet.link.media = '';
|
|
125
|
+
for (const previous of [...sheets.get(href)]) if (previous !== sheet && !reserved(previous)) remove(href, previous);
|
|
126
|
+
}
|
|
127
|
+
if (pinned) handed = new Map();
|
|
128
|
+
for (const [href, records] of sheets) {
|
|
129
|
+
for (const sheet of [...records]) if ((!wanted.has(href) || !sheet.active) && !reserved(sheet)) remove(href, sheet);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
release() {
|
|
133
|
+
const revision = ++cleanup;
|
|
134
|
+
// A normal tree update or StrictMode's effect replay commits in the same
|
|
135
|
+
// turn. Only a genuine fatal/global unmount retires the normal styles.
|
|
136
|
+
queueMicrotask(() => {
|
|
137
|
+
if (cleanup !== revision) return;
|
|
138
|
+
const recovery = new Set([...sheets.values()].flat().filter(sheet => sheet.ready && (sheet.active || reserved(sheet))));
|
|
139
|
+
for (const [href, records] of sheets) for (const sheet of [...records]) {
|
|
140
|
+
// Local reset is allowed to remount the same server tree without a
|
|
141
|
+
// request. Keep already loaded sheets inert so that reset can restore
|
|
142
|
+
// them synchronously, rather than painting before a second load event.
|
|
143
|
+
if (recovery.has(sheet)) { sheet.active = false; sheet.recovery = true; sheet.link.media = 'not all'; }
|
|
144
|
+
else if (recovery.size || !sheet.recovery) remove(href, sheet);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
cancel() {
|
|
149
|
+
for (const [href, records] of sheets) for (const sheet of [...records]) if (!sheet.active && !sheet.recovery && !reserved(sheet)) remove(href, sheet);
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
managers.set(document, manager);
|
|
153
|
+
return manager;
|
|
154
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {open,rm} from 'node:fs/promises';
|
|
2
|
+
import {randomUUID} from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {rename} from './fs.mjs';
|
|
5
|
+
|
|
6
|
+
// The old destination stays readable until the complete replacement exists.
|
|
7
|
+
// Keeping an injectable I/O boundary lets tests exercise real partial-write and
|
|
8
|
+
// rename failures without depending on an antivirus or a physical disk fault.
|
|
9
|
+
export function createAtomicFileWriter({openFile=open,moveFile=rename,removeFile=rm}={}){
|
|
10
|
+
return async function writeAtomicFile(filename,data){
|
|
11
|
+
const temporary=filename+'.'+randomUUID()+'.tmp';let file,created=false,failure,phase='create';
|
|
12
|
+
try{
|
|
13
|
+
file=await openFile(temporary,'wx',0o600);created=true;phase='write';await file.writeFile(data);phase='close';await file.close();file=undefined;
|
|
14
|
+
phase='publish';await moveFile(temporary,filename);
|
|
15
|
+
}catch(error){
|
|
16
|
+
error.atomicFile={operation:'publish-development-state',phase,code:error.code||'UNKNOWN',destination:path.basename(filename),...(error.atomicRename||{})};
|
|
17
|
+
failure=error;
|
|
18
|
+
throw error;
|
|
19
|
+
}finally{
|
|
20
|
+
await file?.close().catch(()=>{});
|
|
21
|
+
// This unpredictable temporary is owned by this invocation. Never remove
|
|
22
|
+
// the destination to work around a Windows lock.
|
|
23
|
+
if(created)try{await removeFile(temporary,{force:true});}catch(error){if(failure)failure.atomicFile.cleanupCode=error.code||'UNKNOWN';else throw error;}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export const writeAtomicFile=createAtomicFileWriter();
|
|
@@ -44,7 +44,10 @@ globalThis[codecSymbol] = {
|
|
|
44
44
|
const bundler = flightBundler(context.clientModules, false);
|
|
45
45
|
let failure;
|
|
46
46
|
// React calls then inside its rendering context, preserving React.cache isolation.
|
|
47
|
-
|
|
47
|
+
// React can subscribe more than once while unwrapping a thenable. Start
|
|
48
|
+
// lazily in its rendering context, but never execute the producer twice.
|
|
49
|
+
let execution;
|
|
50
|
+
const result = { then(resolve, reject) { (execution ||= Promise.resolve().then(callback)).then(resolve, reject); } };
|
|
48
51
|
const stream = renderToReadableStream(result, bundler, { signal: context.signal, temporaryReferences: encoded.serverTemporaryReferences, onError(error) { failure ||= error; return 'PRNextCacheRenderError'; } });
|
|
49
52
|
const chunks = [];
|
|
50
53
|
let size = 0;
|
|
@@ -54,7 +57,9 @@ globalThis[codecSymbol] = {
|
|
|
54
57
|
chunks.push(Buffer.from(value));
|
|
55
58
|
}
|
|
56
59
|
if (failure) throw failure;
|
|
57
|
-
|
|
60
|
+
// Each chunk above is already an owned copy. A single chunk needs no
|
|
61
|
+
// second allocation; never expose the producer's mutable stream storage.
|
|
62
|
+
return chunks.length===1?chunks[0]:Buffer.concat(chunks, size);
|
|
58
63
|
},
|
|
59
64
|
async decode(bytes, encoded, context) {
|
|
60
65
|
return createFromReadableStream(new ReadableStream({ start(controller) { controller.enqueue(bytes); controller.close(); } }), {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {reportRequestError} from './instrumentation.mjs';
|
|
1
2
|
import { runRequestContext, currentRequest } from '../compat/headers.cjs';
|
|
2
3
|
import { flushCacheInvalidations, flushCacheWork } from '../compat/data-cache.cjs';
|
|
3
4
|
import { createAfterScope } from '../compat/after.cjs';
|
|
@@ -9,7 +10,7 @@ export function withCacheRequest(options, phase, callback) {
|
|
|
9
10
|
// only needs metadata, and may survive the response for stale revalidation.
|
|
10
11
|
return runRequestContext({ ...options, body: undefined, routePattern: options.route?.pattern, cacheConfig: options.route?.cacheConfig, phase }, async () => {
|
|
11
12
|
const context = currentRequest();
|
|
12
|
-
if (phase === 'route' && options.route?.cacheConfig?.runtime !== 'edge') context.afterScope = createAfterScope({ signal: options.signal, timeoutMs: options.timeoutMs ?? (options.route?.cacheConfig?.maxDuration ? options.route.cacheConfig.maxDuration * 1000 : 25000) });
|
|
13
|
+
if (phase === 'route' && options.route?.cacheConfig?.runtime !== 'edge') context.afterScope = createAfterScope({report:error=>{console.error('[prnext] after() failed:',error?.stack||error);return reportRequestError(error);}, signal: options.signal, timeoutMs: options.timeoutMs ?? (options.route?.cacheConfig?.maxDuration ? options.route.cacheConfig.maxDuration * 1000 : 25000) });
|
|
13
14
|
let finalized;
|
|
14
15
|
const finalizeCache = () => finalized ||= (async () => { await context.afterScope?.finish(); await flushCacheWork(context); })();
|
|
15
16
|
// The worker owns response completion even when application code throws.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const HEADER = 'x-prnext-deployment-id';
|
|
2
|
+
|
|
3
|
+
/** A generation is an artifact identity, independent from a user-supplied buildId. */
|
|
4
|
+
export function deploymentHeaders(id) { return id ? { [HEADER]: id } : {}; }
|
|
5
|
+
|
|
6
|
+
export async function assertDeploymentResponse(response, expected) {
|
|
7
|
+
if (response.headers.get('x-prnext-document-navigation') === '1') {
|
|
8
|
+
await response.body?.cancel().catch(() => {});
|
|
9
|
+
const error = new Error('This route requires a full document navigation.');
|
|
10
|
+
error.code = 'PRNEXT_DOCUMENT_NAVIGATION';
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
if (!expected) return;
|
|
14
|
+
if (response.headers.get(HEADER) === expected && response.status !== 409) return;
|
|
15
|
+
await response.body?.cancel().catch(() => {});
|
|
16
|
+
const error = new Error('This page belongs to a previous deployment. Reload the page before continuing.');
|
|
17
|
+
error.code = 'PRNEXT_DEPLOYMENT_CHANGED';
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
package/runtime/dev-client.mjs
CHANGED
|
@@ -195,11 +195,18 @@ async function applyUpdate(message, {navigation=false}={}) {
|
|
|
195
195
|
const route = kind === 'pages' ? manifest.pages.find(route => route.pattern === application.router.snapshot().pathname) : null;
|
|
196
196
|
const entry = kind === 'pages' ? route?.client : manifest.app?.client;
|
|
197
197
|
if (!entry) { reload('The current route changed. Page reloaded.'); return; }
|
|
198
|
-
if(!message.cssOnly)
|
|
198
|
+
if(!message.cssOnly){
|
|
199
|
+
await import(/* webpackIgnore: true */ entry);
|
|
200
|
+
// Activated deferred modules must register their updated component
|
|
201
|
+
// families before Refresh runs, even when their loader is already cached.
|
|
202
|
+
await Promise.all((manifest.lazy||[]).map(module=>import(/* webpackIgnore: true */ module.url)));
|
|
203
|
+
}
|
|
199
204
|
const pending = state.pending;
|
|
200
205
|
if (pending) await application.devPrepare?.(pending.options);
|
|
201
206
|
if (state.unsafe) { reload('An edited module exports values outside React components. Page reloaded.'); return; }
|
|
202
|
-
|
|
207
|
+
if (kind === 'app' && application.devPrepareStyles) await application.devPrepareStyles(manifest.app.routes);
|
|
208
|
+
else await updateStyles([...(kind === 'pages' ? route.css : application.devStyles?.(manifest.app.routes) || manifest.app.css),...(manifest.lazy||[]).flatMap(module=>module.css)]);
|
|
209
|
+
if (kind === 'app') application.devStylesApplied?.(manifest.app.routes, { commit: !!message.cssOnly || !pending });
|
|
203
210
|
// An unrelated type-only edit may publish a new generation without changing
|
|
204
211
|
// any browser module. Keep its existing runtime/hydration issues and current
|
|
205
212
|
// selection until application code changes, rather than racing diagnostics.
|
|
@@ -287,7 +294,10 @@ function connect(options) {
|
|
|
287
294
|
if (message.buildId === state.buildId) return;
|
|
288
295
|
// A cold navigation may receive SSE readiness before its ensure response.
|
|
289
296
|
// Updating modules is safe; refreshing the old route would cancel the click.
|
|
290
|
-
|
|
297
|
+
// A retained branch can become cold after ensure() has returned. Its
|
|
298
|
+
// worker-triggered compilation is still part of the pending navigation;
|
|
299
|
+
// committing a refresh here would abort it and restore the previous URL.
|
|
300
|
+
void applyUpdate(message,{navigation:state.ensuring>0||message.reason?.code==='route-opened'});
|
|
291
301
|
};
|
|
292
302
|
globalThis.addEventListener('error', event => { if (event.error) showError(event.error); });
|
|
293
303
|
globalThis.addEventListener('unhandledrejection', event => showError(event.reason));
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import {randomBytes,timingSafeEqual} from 'node:crypto';
|
|
3
|
+
import {readFile,realpath} from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
// Private control plane. Request headers and bodies never cross this endpoint.
|
|
7
|
+
export async function createDevCompilerBroker({compile,maxPending=64}) {
|
|
8
|
+
const token=randomBytes(32).toString('base64url');let pending=0;
|
|
9
|
+
const server=http.createServer(async(req,res)=>{
|
|
10
|
+
const supplied=Buffer.from(req.headers.authorization||''),expected=Buffer.from('Bearer '+token);
|
|
11
|
+
if(req.method!=='POST'||req.url!=='/compile'||supplied.length!==expected.length||!timingSafeEqual(supplied,expected)){res.writeHead(403).end();return;}
|
|
12
|
+
if(pending>=maxPending){res.writeHead(503).end();return;}
|
|
13
|
+
pending++;
|
|
14
|
+
try{
|
|
15
|
+
const chunks=[];let bytes=0;
|
|
16
|
+
for await(const chunk of req){bytes+=chunk.length;if(bytes>65536)throw Error('Compiler request too large');chunks.push(chunk);}
|
|
17
|
+
// Decode once: an HTTP chunk boundary may split a UTF-8 route name.
|
|
18
|
+
const input=JSON.parse(Buffer.concat(chunks,bytes).toString('utf8'));if(typeof input.pattern!=='string'||!input.pattern.startsWith('/')||input.pattern.length>4096)throw Error('Invalid route pattern');
|
|
19
|
+
if(input.sources!==undefined&&(!Array.isArray(input.sources)||input.sources.length>32||!input.sources.every(p=>typeof p==='string'&&p.startsWith('/')&&p.length<=4096)))throw Error('Invalid retained route patterns');
|
|
20
|
+
const directory=await compile(input.pattern,input.sources||[]);
|
|
21
|
+
res.writeHead(200,{'content-type':'application/json','cache-control':'no-store'}).end(JSON.stringify({directory}));
|
|
22
|
+
}catch(error){res.writeHead(500,{'content-type':'application/json'}).end(JSON.stringify({error:String(error.message).slice(0,2048)}));}
|
|
23
|
+
finally{pending--;}
|
|
24
|
+
});
|
|
25
|
+
server.requestTimeout=125000;server.headersTimeout=5000;
|
|
26
|
+
await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(0,'127.0.0.1',resolve);});
|
|
27
|
+
return {descriptor:JSON.stringify({port:server.address().port,token}),close:()=>new Promise(resolve=>{server.close(resolve);server.closeIdleConnections();})};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function deferredCompiler(descriptor,{root,home}) {
|
|
31
|
+
const value=JSON.parse(descriptor);
|
|
32
|
+
if(!Number.isInteger(value.port)||value.port<1||value.port>65535||!/^[\w-]{43}$/.test(value.token))throw Error('Invalid private compiler endpoint');
|
|
33
|
+
return async(pattern,signal,sources=[])=>{
|
|
34
|
+
const response=await fetch(`http://127.0.0.1:${value.port}/compile`,{method:'POST',headers:{authorization:'Bearer '+value.token,'content-type':'application/json'},body:JSON.stringify({pattern,sources}),signal:AbortSignal.any([...(signal?[signal]:[]),AbortSignal.timeout(120000)])});
|
|
35
|
+
const result=await response.json();if(!response.ok)throw Error(result.error||'Deferred route compilation failed');
|
|
36
|
+
const directory=await realpath(path.resolve(root,result.directory)),parent=await realpath(home),relative=path.relative(parent,directory);
|
|
37
|
+
if(!relative||relative.startsWith('..')||path.isAbsolute(relative))throw Error('Compiler generation escaped its development session');
|
|
38
|
+
const manifest=JSON.parse(await readFile(path.join(directory,'manifest.json'),'utf8'));
|
|
39
|
+
if(!manifest.dev||manifest.devSession!==path.basename(parent))throw Error('Invalid development generation');
|
|
40
|
+
return {distDir:directory,manifest};
|
|
41
|
+
};
|
|
42
|
+
}
|