@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,108 @@
|
|
|
1
|
+
export const MAX_STREAM_CHUNK = 64 * 1024;
|
|
2
|
+
const MAX_CONTROL_BYTES = 64 * 1024;
|
|
3
|
+
const NO_BODY = new Set([204, 205, 304]);
|
|
4
|
+
|
|
5
|
+
function bytes(value) {
|
|
6
|
+
if (typeof value === 'string') return Buffer.from(value);
|
|
7
|
+
if (value instanceof ArrayBuffer) return Buffer.from(value);
|
|
8
|
+
if (ArrayBuffer.isView(value)) return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
9
|
+
throw new TypeError('A response stream must yield bytes');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function* responseChunks(body) {
|
|
13
|
+
if (body === null || body === undefined) return;
|
|
14
|
+
if (typeof body === 'string' || body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
|
|
15
|
+
const value = bytes(body);
|
|
16
|
+
for (let offset = 0; offset < value.length; offset += MAX_STREAM_CHUNK) yield value.subarray(offset, offset + MAX_STREAM_CHUNK);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (typeof body[Symbol.asyncIterator] !== 'function' && typeof body[Symbol.iterator] !== 'function') {
|
|
20
|
+
throw new TypeError('Invalid response stream');
|
|
21
|
+
}
|
|
22
|
+
for await (const item of body) {
|
|
23
|
+
const value = bytes(item);
|
|
24
|
+
for (let offset = 0; offset < value.length; offset += MAX_STREAM_CHUNK) yield value.subarray(offset, offset + MAX_STREAM_CHUNK);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// A resolved write means the pipe has accepted the bytes. Awaiting every frame
|
|
29
|
+
// prevents Node's writable queue from growing with a slow Rust/HTTP consumer.
|
|
30
|
+
export function createProtocolOutput(write, { cork, uncork } = {}) {
|
|
31
|
+
const send = value => new Promise((resolve, reject) => {
|
|
32
|
+
write(value, error => error ? reject(error) : resolve());
|
|
33
|
+
});
|
|
34
|
+
const controlLine = frame => {
|
|
35
|
+
const value = JSON.stringify(frame) + '\n';
|
|
36
|
+
if (Buffer.byteLength(value) > MAX_CONTROL_BYTES) throw new Error('Response headers exceed the 64 KiB transport limit');
|
|
37
|
+
return value;
|
|
38
|
+
};
|
|
39
|
+
const control = frame => send(controlLine(frame));
|
|
40
|
+
const sendParts = parts => new Promise((resolve, reject) => {
|
|
41
|
+
cork();
|
|
42
|
+
try {
|
|
43
|
+
for (let index = 0; index < parts.length; index++) {
|
|
44
|
+
write(parts[index], error => {
|
|
45
|
+
if (error) reject(error);
|
|
46
|
+
else if (index === parts.length - 1) resolve();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
} catch (error) { reject(error); }
|
|
50
|
+
finally { uncork(); }
|
|
51
|
+
});
|
|
52
|
+
const sendChunk = (id, chunk) => {
|
|
53
|
+
const line = controlLine({ id, type: 'chunk', length: chunk.length });
|
|
54
|
+
if (!cork || !uncork) return send(line).then(() => send(chunk));
|
|
55
|
+
// writev sends metadata and bytes without joining/copying the payload.
|
|
56
|
+
return sendParts([line, chunk]);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return async function output(id, result, { stream = false, compactResponse = false, method = 'GET', production = true, signalController } = {}) {
|
|
60
|
+
const responseBody = result.bufferedBody ?? result.body;
|
|
61
|
+
const pageError = { ...(result.pageError ? { pageError: result.pageError } : {}),
|
|
62
|
+
...(result.pageFailure ? { pageFailure: result.pageFailure } : {}) };
|
|
63
|
+
if (stream && !result.isr && (method === 'HEAD' || NO_BODY.has(result.status))) {
|
|
64
|
+
await result.cancel?.();
|
|
65
|
+
if (result.body?.cancel && !result.body.locked) await result.body.cancel();
|
|
66
|
+
await send(JSON.stringify({ id, status: result.status, headers: result.headers || {}, body: '', ...pageError }) + '\n');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (!stream && !result.isr) {
|
|
70
|
+
await send(JSON.stringify({ id, status: result.status, headers: result.headers || {}, body: Buffer.from(result.body || '').toString('base64'), ...pageError }) + '\n');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const headers = Object.fromEntries(Object.entries(result.headers || {}).filter(([name]) => name.toLowerCase() !== 'content-length'));
|
|
74
|
+
if (compactResponse && !result.isr && (typeof responseBody === 'string' || responseBody instanceof ArrayBuffer || ArrayBuffer.isView(responseBody))) {
|
|
75
|
+
const body = bytes(responseBody);
|
|
76
|
+
if (body.length <= 16 * 1024) {
|
|
77
|
+
const complete = controlLine({ id, type: 'complete', status: result.status, headers, length: body.length, ...pageError });
|
|
78
|
+
if (cork && uncork) await sendParts(body.length ? [complete, body] : [complete]);
|
|
79
|
+
else { await send(complete); if (body.length) await send(body); }
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const head = controlLine({ id, type: 'head', status: result.status, headers, ...pageError, ...(result.isr ? { isr: result.isr } : {}) });
|
|
84
|
+
if (cork && uncork && (typeof responseBody === 'string' || responseBody instanceof ArrayBuffer || ArrayBuffer.isView(responseBody))) {
|
|
85
|
+
const body = bytes(responseBody);
|
|
86
|
+
if (body.length <= MAX_STREAM_CHUNK) {
|
|
87
|
+
// Pages HTML and buffered action responses already know their entire
|
|
88
|
+
// small body. Send their frames together, keeping the same wire format.
|
|
89
|
+
await sendParts([head, ...(body.length ? [controlLine({id,type:'chunk',length:body.length}),body] : []), controlLine({id,type:'end'})]);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
await send(head);
|
|
94
|
+
try {
|
|
95
|
+
for await (const chunk of responseChunks(responseBody)) {
|
|
96
|
+
await sendChunk(id, chunk);
|
|
97
|
+
}
|
|
98
|
+
await control({ id, type: 'end' });
|
|
99
|
+
} catch (error) {
|
|
100
|
+
signalController?.abort(error);
|
|
101
|
+
await result.cancel?.(error);
|
|
102
|
+
console.error('[prnext] Response stream failed:', error?.stack || error);
|
|
103
|
+
// Headers are already on the wire: terminate this response, never emit a
|
|
104
|
+
// replacement HTTP response or retry application code after a mutation.
|
|
105
|
+
await control({ id, type: 'error', message: production ? 'Response stream failed' : String(error?.message || error).slice(0, 4096) });
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { watch as watchDirectory } from 'node:fs';
|
|
2
|
+
import { readdir } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { shouldWatchProjectFile } from './env.mjs';
|
|
5
|
+
|
|
6
|
+
const missing = error => error.code === 'ENOENT' || error.code === 'ENOTDIR';
|
|
7
|
+
|
|
8
|
+
// On Linux Node implements recursive fs.watch by scanning every descendant,
|
|
9
|
+
// including ignored build trees. Atomic build swaps can delete a descendant
|
|
10
|
+
// during that scan. Watch only source directories, and handle removal races
|
|
11
|
+
// in our own traversal. No file contents or node_modules watches are retained.
|
|
12
|
+
export async function watchProject(root, { onChange, onError, ignore = () => false, watch = watchDirectory, maxDirectories = 8192, nativeRecursive = process.platform === 'darwin' }) {
|
|
13
|
+
// macOS has native recursive FSEvents support. Retain that backend: separate
|
|
14
|
+
// directory subscriptions can lose coalesced events during rapid renames.
|
|
15
|
+
if (nativeRecursive) {
|
|
16
|
+
const watcher = watch(root, { recursive: true }, (_event, filename) => {
|
|
17
|
+
const relative = filename && String(filename).replaceAll(path.sep, '/');
|
|
18
|
+
if (relative && !ignore(relative) && shouldWatchProjectFile(relative)) onChange(relative);
|
|
19
|
+
});
|
|
20
|
+
watcher.on('error', error => { watcher.close(); onError(error); });
|
|
21
|
+
return { close: () => watcher.close(), async refresh() {} };
|
|
22
|
+
}
|
|
23
|
+
const directories = new Map();
|
|
24
|
+
const pending = new Set();
|
|
25
|
+
let closed = false, ready = false, timer, running, refreshAll = false;
|
|
26
|
+
const allowedDirectory = relative => !relative || (!ignore(relative) && (relative === '.contentlayer' || shouldWatchProjectFile(relative)));
|
|
27
|
+
const allowedFile = relative => relative && !ignore(relative) && shouldWatchProjectFile(relative);
|
|
28
|
+
function remove(relative) {
|
|
29
|
+
for (const [name, watcher] of directories) if (name === relative || name.startsWith(relative + '/')) {
|
|
30
|
+
directories.delete(name);
|
|
31
|
+
watcher.close();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function close() {
|
|
35
|
+
closed = true;
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
pending.clear();
|
|
38
|
+
for (const watcher of directories.values()) watcher.close();
|
|
39
|
+
directories.clear();
|
|
40
|
+
}
|
|
41
|
+
function fail(error) {
|
|
42
|
+
if (closed) return;
|
|
43
|
+
close();
|
|
44
|
+
onError(error);
|
|
45
|
+
}
|
|
46
|
+
function queue(relative) {
|
|
47
|
+
if (closed) return;
|
|
48
|
+
pending.add(relative);
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
timer = setTimeout(() => { void drain().catch(fail); }, 20);
|
|
51
|
+
}
|
|
52
|
+
function disappeared(relative) {
|
|
53
|
+
remove(relative);
|
|
54
|
+
// A rapid rename can be coalesced by macOS. If a scheduled scan observes
|
|
55
|
+
// the old path disappearing, reconcile its parent to discover the new one.
|
|
56
|
+
queue(path.posix.dirname(relative) === '.' ? '' : path.posix.dirname(relative));
|
|
57
|
+
}
|
|
58
|
+
async function scan(relative, recursive = false) {
|
|
59
|
+
if (closed) return;
|
|
60
|
+
if (!allowedDirectory(relative)) { remove(relative); return; }
|
|
61
|
+
if (!directories.has(relative)) {
|
|
62
|
+
if (directories.size >= maxDirectories) throw new Error(`Development watcher exceeds ${maxDirectories} source directories. Narrow the application directory.`);
|
|
63
|
+
let watcher;
|
|
64
|
+
try {
|
|
65
|
+
watcher = watch(path.join(root, relative), { recursive: false }, (event, filename) => {
|
|
66
|
+
if (closed) return;
|
|
67
|
+
if (relative && event === 'rename' && String(filename) === path.posix.basename(relative)) {
|
|
68
|
+
// inotify subscriptions follow an inode. Reopen this subtree even
|
|
69
|
+
// when an atomic source replacement already recreated its path.
|
|
70
|
+
disappeared(relative);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const changed = filename ? path.posix.join(relative, String(filename).replaceAll(path.sep, '/')) : relative;
|
|
74
|
+
if (allowedFile(changed)) onChange(changed);
|
|
75
|
+
// The .contentlayer container itself is ignored as a source, but
|
|
76
|
+
// its generated directory may be created after the watcher starts.
|
|
77
|
+
// Platforms can coalesce a directory rename into a change event.
|
|
78
|
+
// Rescan only this directory, rather than relying on the event name.
|
|
79
|
+
if (allowedDirectory(changed) || allowedFile(changed)) queue(relative);
|
|
80
|
+
});
|
|
81
|
+
} catch (error) { if (relative && missing(error)) { disappeared(relative); return; } throw error; }
|
|
82
|
+
directories.set(relative, watcher);
|
|
83
|
+
// Files may already exist by the time a newly created directory is
|
|
84
|
+
// subscribed. Its discovery must also invalidate the source snapshot.
|
|
85
|
+
if (ready && allowedFile(relative)) onChange(relative);
|
|
86
|
+
watcher.on('error', error => {
|
|
87
|
+
if (closed) return;
|
|
88
|
+
if (relative && missing(error)) disappeared(relative);
|
|
89
|
+
else fail(error);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
let entries;
|
|
93
|
+
try { entries = await readdir(path.join(root, relative), { withFileTypes: true }); }
|
|
94
|
+
catch (error) { if (missing(error) && relative) { disappeared(relative); return; } throw error; }
|
|
95
|
+
if (closed) return;
|
|
96
|
+
const children = new Set();
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
const child = path.posix.join(relative, entry.name);
|
|
99
|
+
if (!entry.isDirectory() || !allowedDirectory(child)) continue;
|
|
100
|
+
children.add(child);
|
|
101
|
+
if (recursive || !directories.has(child)) await scan(child, recursive);
|
|
102
|
+
}
|
|
103
|
+
for (const name of directories.keys()) {
|
|
104
|
+
if (name && (path.posix.dirname(name) === '.' ? '' : path.posix.dirname(name)) === relative && !children.has(name)) remove(name);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function drain() {
|
|
108
|
+
if (running) return running;
|
|
109
|
+
running = (async () => {
|
|
110
|
+
while (!closed && pending.size) {
|
|
111
|
+
const batch = [...pending]; pending.clear();
|
|
112
|
+
const recursive = refreshAll; refreshAll = false;
|
|
113
|
+
for (const relative of batch) await scan(relative, recursive);
|
|
114
|
+
}
|
|
115
|
+
})().finally(() => { running = undefined; });
|
|
116
|
+
return running;
|
|
117
|
+
}
|
|
118
|
+
try { await scan('', true); }
|
|
119
|
+
catch (error) { close(); throw error; }
|
|
120
|
+
ready = true;
|
|
121
|
+
return {
|
|
122
|
+
close,
|
|
123
|
+
async refresh() {
|
|
124
|
+
if (closed) return;
|
|
125
|
+
refreshAll = true;
|
|
126
|
+
pending.add('');
|
|
127
|
+
await drain();
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Immutable build artefacts only. IDs and evictions use the request's MessagePort
|
|
2
|
+
// so a receiver resolves an ID before a later eviction can remove its entry.
|
|
3
|
+
export function workerArtifacts(port, { maxEntries = 8, maxBytes = 256 * 1024, maxEntryBytes = 64 * 1024 } = {}) {
|
|
4
|
+
const entries = new Map();
|
|
5
|
+
let sequence = 0, bytes = 0;
|
|
6
|
+
return flight => {
|
|
7
|
+
if (typeof flight !== 'string') return undefined;
|
|
8
|
+
const size = flight.length * 2;
|
|
9
|
+
if (!maxEntries || size > maxBytes || size > maxEntryBytes) return undefined;
|
|
10
|
+
const old = entries.get(flight);
|
|
11
|
+
if (old) { entries.delete(flight); entries.set(flight, old); return old.id; }
|
|
12
|
+
while (entries.size && (entries.size >= maxEntries || bytes + size > maxBytes)) {
|
|
13
|
+
const [key, item] = entries.entries().next().value;
|
|
14
|
+
port.postMessage({ type: 'artifact-drop', id: item.id });
|
|
15
|
+
entries.delete(key); bytes -= item.size;
|
|
16
|
+
}
|
|
17
|
+
const id = ++sequence;
|
|
18
|
+
port.postMessage({ type: 'artifact', id, value: flight });
|
|
19
|
+
entries.set(flight, { id, size }); bytes += size;
|
|
20
|
+
return id;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Only build manifests belong here. Never register headers, models, closures or
|
|
2
|
+
// request contexts. Eviction is ordered on the same MessagePort as requests.
|
|
3
|
+
export function workerMetadata(port, limit = 32) {
|
|
4
|
+
const entries = new Map();
|
|
5
|
+
let sequence = 0;
|
|
6
|
+
return value => {
|
|
7
|
+
if (!value || typeof value !== 'object') return undefined;
|
|
8
|
+
let id = entries.get(value);
|
|
9
|
+
if (id !== undefined) { entries.delete(value); entries.set(value, id); return id; }
|
|
10
|
+
id = ++sequence;
|
|
11
|
+
if (entries.size >= limit) {
|
|
12
|
+
const [old, oldId] = entries.entries().next().value;
|
|
13
|
+
port.postMessage({ type: 'metadata-drop', id: oldId });
|
|
14
|
+
entries.delete(old);
|
|
15
|
+
}
|
|
16
|
+
port.postMessage({ type: 'metadata', id, value });
|
|
17
|
+
entries.set(value, id);
|
|
18
|
+
return id;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const bundlers = new WeakMap();
|
|
23
|
+
const empty = Object.freeze({});
|
|
24
|
+
export function flightBundler(clientModules = empty, browser = true) {
|
|
25
|
+
let pair = bundlers.get(clientModules);
|
|
26
|
+
if (!pair) { pair = []; bundlers.set(clientModules, pair); }
|
|
27
|
+
const slot = browser ? 1 : 0;
|
|
28
|
+
if (!pair[slot]) pair[slot] = Object.fromEntries(Object.entries(clientModules).map(([id, item]) =>
|
|
29
|
+
[id, { id, chunks: browser && item.browserModule ? [id, item.browserModule] : [], name: '*' }]));
|
|
30
|
+
return pair[slot];
|
|
31
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// prnext-transport:socket-v2
|
|
2
|
+
// prnext-concurrency:512
|
|
3
|
+
import { requestFrames } from './request-transport.mjs';
|
|
4
|
+
import { requestWork } from './request-work.mjs';
|
|
5
|
+
import { AsyncResource } from 'node:async_hooks';
|
|
6
|
+
const workScope = new AsyncResource('PRNextWorkObserver', { triggerAsyncId: 0 });
|
|
7
|
+
import { readFile } from 'node:fs/promises';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { pathToFileURL } from 'node:url';
|
|
10
|
+
import readline from 'node:readline';
|
|
11
|
+
import net from 'node:net';
|
|
12
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
13
|
+
import { Console } from 'node:console';
|
|
14
|
+
import { navigationResponse } from './navigation.mjs';
|
|
15
|
+
import { createProtocolOutput } from './transport.mjs';
|
|
16
|
+
import { loadEnvConfig } from './env.mjs';
|
|
17
|
+
|
|
18
|
+
// stdout is exclusively the transport, including when user packages log during import.
|
|
19
|
+
const writeProtocol = process.stdout.write.bind(process.stdout);
|
|
20
|
+
process.stdout.write = process.stderr.write.bind(process.stderr);
|
|
21
|
+
globalThis.console = new Console({ stdout: process.stderr, stderr: process.stderr });
|
|
22
|
+
|
|
23
|
+
const projectRoot = path.resolve(process.argv[2] || process.cwd());
|
|
24
|
+
const distDir = path.resolve(projectRoot, process.argv[3] || '.prnext');
|
|
25
|
+
process.chdir(projectRoot);
|
|
26
|
+
let manifest;
|
|
27
|
+
try { manifest = JSON.parse(await readFile(path.join(distDir, 'manifest.json'), 'utf8')); }
|
|
28
|
+
catch (error) { console.error('PRNext worker could not read build manifest:', error.message); process.exit(1); }
|
|
29
|
+
process.env.NODE_ENV ??= manifest.dev ? 'development' : 'production';
|
|
30
|
+
loadEnvConfig(projectRoot, { dev: !!manifest.dev });
|
|
31
|
+
// NODE_ENV=test selects .env.test above. React itself must match the compiled
|
|
32
|
+
// browser graph and the RSC worker's mode, including dev launched from CI.
|
|
33
|
+
process.env.NODE_ENV = manifest.dev ? 'development' : 'production';
|
|
34
|
+
const production = !manifest.dev;
|
|
35
|
+
// Load each renderer beside the application only when that request needs it.
|
|
36
|
+
// Middleware and API workers do not pay for React or the Pages/App renderers.
|
|
37
|
+
const runtimeModule = name => import(pathToFileURL(path.join(distDir, `runtime/${name}.mjs`)).href);
|
|
38
|
+
let errorResponse;
|
|
39
|
+
try { ({ errorResponse } = await runtimeModule('http')); }
|
|
40
|
+
catch (error) { console.error('PRNext worker could not load the project runtime:', error.message); process.exit(1); }
|
|
41
|
+
const routes = new Map(manifest.routes.map(route => [route.id, route]));
|
|
42
|
+
const prerendered = new Set((manifest.prerendered || []).map(item => item.path));
|
|
43
|
+
let appRuntime;
|
|
44
|
+
let appStaticRuntime;
|
|
45
|
+
let routeStaticRuntime;
|
|
46
|
+
let middlewareRuntime;
|
|
47
|
+
let pagesRuntime;
|
|
48
|
+
let apiRuntime;
|
|
49
|
+
let incrementalRuntime;
|
|
50
|
+
|
|
51
|
+
async function handleRequest(input, output, signalController) {
|
|
52
|
+
let responseStarted = false;
|
|
53
|
+
try {
|
|
54
|
+
if (input.renderMode === 'incremental-cache' && input.routeId === '__prnext_incremental_cache') {
|
|
55
|
+
incrementalRuntime ??= await runtimeModule('incremental-cache');
|
|
56
|
+
const result = await incrementalRuntime.runIncrementalCache({ ...input, manifest, distDir, production, signal: signalController.signal });
|
|
57
|
+
input.body = undefined;
|
|
58
|
+
responseStarted = true;
|
|
59
|
+
await output(input.id, result, { ...input, production, signalController });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const isMiddleware = input.renderMode === 'middleware' && input.routeId === '__prnext_middleware';
|
|
63
|
+
const route = isMiddleware && manifest.middleware
|
|
64
|
+
? { ...manifest.middleware, id: '__prnext_middleware', kind: 'middleware' }
|
|
65
|
+
: routes.get(input.routeId);
|
|
66
|
+
if (!route) { await output(input.id, { status: 404, body: 'Not Found' }, input); return; }
|
|
67
|
+
const requestedPath = new URL(input.url).pathname;
|
|
68
|
+
if (route.router !== 'app' && !['isr', 'error404', 'error500'].includes(input.renderMode) && route.fallback === false && !prerendered.has(requestedPath) && !prerendered.has(requestedPath.replace(/\/$/, '') || '/')) {
|
|
69
|
+
await output(input.id, { status: 404, body: 'Not Found' }, input); return;
|
|
70
|
+
}
|
|
71
|
+
const options = { ...input, modulePath: path.resolve(distDir, route.module), route, production, manifest, distDir, signal: signalController.signal };
|
|
72
|
+
let result;
|
|
73
|
+
if (isMiddleware) {
|
|
74
|
+
middlewareRuntime ??= await import(pathToFileURL(path.join(distDir, 'runtime/middleware.mjs')).href);
|
|
75
|
+
result = await middlewareRuntime.runMiddleware(options);
|
|
76
|
+
} else if (route.router === 'app' && route.kind === 'page') {
|
|
77
|
+
if (input.renderMode === 'isr') {
|
|
78
|
+
appStaticRuntime ??= await import(pathToFileURL(path.join(distDir, 'runtime/app-static.mjs')).href);
|
|
79
|
+
result = await appStaticRuntime.renderAppIsrPage(options);
|
|
80
|
+
} else {
|
|
81
|
+
appRuntime ??= await import(pathToFileURL(path.join(distDir, 'runtime/app-render.mjs')).href);
|
|
82
|
+
result = await appRuntime.renderAppPage(options);
|
|
83
|
+
}
|
|
84
|
+
} else if (route.router === 'app' && route.kind === 'api' && input.renderMode === 'isr') {
|
|
85
|
+
routeStaticRuntime ??= await import(pathToFileURL(path.join(distDir, 'runtime/route-static.mjs')).href);
|
|
86
|
+
result = await routeStaticRuntime.renderRouteHandlerIsr(options);
|
|
87
|
+
} else if (route.kind === 'api') {
|
|
88
|
+
apiRuntime ??= await runtimeModule('api');
|
|
89
|
+
result = await apiRuntime.runApi(options);
|
|
90
|
+
} else {
|
|
91
|
+
pagesRuntime ??= await runtimeModule('render');
|
|
92
|
+
result = input.renderMode === 'isr' ? await pagesRuntime.renderIsrPage({ ...options, capturePageFailure: true })
|
|
93
|
+
: await pagesRuntime.renderPageRequest({ ...options, nativeErrors: true });
|
|
94
|
+
}
|
|
95
|
+
// Long responses should retain only the application's decoded request,
|
|
96
|
+
// not the original JSON line and an extra base64 copy of its body.
|
|
97
|
+
input.body = undefined;
|
|
98
|
+
options.body = undefined;
|
|
99
|
+
responseStarted = true;
|
|
100
|
+
try { await output(input.id, result, { ...input, production, signalController }); }
|
|
101
|
+
finally { await result.finalizeCache?.(); }
|
|
102
|
+
} catch (error) {
|
|
103
|
+
signalController.abort(error);
|
|
104
|
+
if (responseStarted) {
|
|
105
|
+
// A broken pipe or invalid transport cannot be repaired with a second
|
|
106
|
+
// response on the same request. Rust replaces this failed worker.
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
await output(input?.id ?? null, navigationResponse(error) || errorResponse(error, production), { ...input, production, signalController });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Native lanes use separate sockets, so backpressure or cancellation on one
|
|
114
|
+
// response never blocks another response's bytes or aborts its request context.
|
|
115
|
+
const laneLimit = Math.min(512, Math.max(1, Number(process.env.PRNEXT_WORKER_SOCKET) || 1));
|
|
116
|
+
const token = process.env.PRNEXT_WORKER_TOKEN;
|
|
117
|
+
delete process.env.PRNEXT_WORKER_TOKEN;
|
|
118
|
+
delete process.env.PRNEXT_WORKER_SOCKET;
|
|
119
|
+
if (token && laneLimit > 1) {
|
|
120
|
+
const expected = Buffer.from(token);
|
|
121
|
+
const sockets = new Set();
|
|
122
|
+
const invocations = new Set();
|
|
123
|
+
const server = net.createServer(socket => {
|
|
124
|
+
if (sockets.size >= laneLimit) { socket.destroy(); return; }
|
|
125
|
+
sockets.add(socket); socket.setNoDelay(true);
|
|
126
|
+
let controller;
|
|
127
|
+
const authTimer = setTimeout(() => socket.destroy(), 5000);
|
|
128
|
+
socket.on('error', () => {});
|
|
129
|
+
socket.on('close', () => { clearTimeout(authTimer); sockets.delete(socket); });
|
|
130
|
+
// A handler may stop consuming the async iterator while awaiting npm code.
|
|
131
|
+
// Observe EOF even in paused mode without draining or buffering more input.
|
|
132
|
+
socket.on('readable', () => { if (socket.readableLength === 0) socket.read(0); });
|
|
133
|
+
const disconnected = () => {
|
|
134
|
+
const abandoned = controller;
|
|
135
|
+
if (!abandoned || abandoned.signal.aborted) return;
|
|
136
|
+
abandoned.abort(new Error('Request transport closed'));
|
|
137
|
+
|
|
138
|
+
};
|
|
139
|
+
socket.on('end', disconnected);
|
|
140
|
+
socket.on('close', disconnected);
|
|
141
|
+
const output = createProtocolOutput(socket.write.bind(socket), { cork: () => socket.cork(), uncork: () => socket.uncork() });
|
|
142
|
+
void (async () => {
|
|
143
|
+
try {
|
|
144
|
+
for await (const input of requestFrames(socket, { authenticate(line) {
|
|
145
|
+
const candidate = Buffer.from(line);
|
|
146
|
+
if (candidate.length !== expected.length || !timingSafeEqual(candidate, expected)) throw new Error('Worker authentication failed');
|
|
147
|
+
clearTimeout(authTimer); socket.write('ready\n');
|
|
148
|
+
} })) {
|
|
149
|
+
if (invocations.size >= laneLimit) throw new Error('Worker invocation capacity exceeded');
|
|
150
|
+
controller = new AbortController();
|
|
151
|
+
const active = controller;
|
|
152
|
+
let pendingWork = 0, finished = false;
|
|
153
|
+
invocations.add(active);
|
|
154
|
+
active.signal[requestWork] = promise => {
|
|
155
|
+
pendingWork++;
|
|
156
|
+
const settled = () => { if (--pendingWork === 0 && finished) invocations.delete(active); };
|
|
157
|
+
workScope.runInAsyncScope(() => Promise.resolve(promise).then(settled, settled));
|
|
158
|
+
};
|
|
159
|
+
const retireIfAbandoned = () => {
|
|
160
|
+
const timer = setTimeout(() => { if (invocations.has(active)) process.exit(70); }, 5000);
|
|
161
|
+
timer.unref();
|
|
162
|
+
};
|
|
163
|
+
active.signal.addEventListener('abort', retireIfAbandoned, { once: true });
|
|
164
|
+
try { await handleRequest(input, output, active); }
|
|
165
|
+
finally {
|
|
166
|
+
finished = true;
|
|
167
|
+
if (!pendingWork) invocations.delete(active);
|
|
168
|
+
else if (!active.signal.aborted) {
|
|
169
|
+
// Background code after sending a complete response still has a
|
|
170
|
+
// finite lifetime, matching the native request deadline.
|
|
171
|
+
const timer = setTimeout(() => { if (invocations.has(active)) process.exit(70); }, 30000);
|
|
172
|
+
timer.unref();
|
|
173
|
+
}
|
|
174
|
+
controller = undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (!socket.destroyed) console.error('[prnext] Request lane failed:', error?.message || error);
|
|
179
|
+
} finally { socket.destroy(); }
|
|
180
|
+
})();
|
|
181
|
+
});
|
|
182
|
+
// The stdin pipe is a parent-lifetime guard even though requests use sockets.
|
|
183
|
+
process.stdin.resume();
|
|
184
|
+
process.stdin.once('end', () => { for (const socket of sockets) socket.destroy(); server.close(); process.exit(0); });
|
|
185
|
+
server.listen(0, '127.0.0.1', () => {
|
|
186
|
+
writeProtocol(JSON.stringify({ port: server.address().port, binary: true, heartbeat: true }) + '\n');
|
|
187
|
+
setInterval(() => writeProtocol('alive\n'), 1000).unref();
|
|
188
|
+
});
|
|
189
|
+
} else {
|
|
190
|
+
const output = createProtocolOutput(writeProtocol, { cork: () => process.stdout.cork(), uncork: () => process.stdout.uncork() });
|
|
191
|
+
for await (let line of readline.createInterface({ input: process.stdin, crlfDelay: Infinity })) {
|
|
192
|
+
if (!line.trim()) continue;
|
|
193
|
+
const input = JSON.parse(line); line = '';
|
|
194
|
+
try { await handleRequest(input, output, new AbortController()); }
|
|
195
|
+
catch (error) { console.error('[prnext] Worker transport failed:', error?.stack || error); process.exit(1); }
|
|
196
|
+
}
|
|
197
|
+
}
|