@thomas.f/prnext 0.1.12-alpha → 0.1.15-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +305 -65
- package/build/ADAPTERS.md +121 -0
- package/build/actions.mjs +4 -0
- package/build/app-config.mjs +2 -0
- package/build/app.mjs +23 -5
- package/build/build-lock.mjs +71 -0
- package/build/bundle-runtime.mjs +82 -0
- package/build/compiler.mjs +3 -1
- package/build/config.mjs +9 -2
- package/build/font-cache.mjs +50 -0
- package/build/font-files.mjs +5 -3
- package/build/fonts.mjs +1 -1
- package/build/index.mjs +12 -1
- package/build/inspect.mjs +20 -2
- package/build/inspection-summary.mjs +37 -0
- package/build/metadata-conflicts.mjs +13 -0
- package/build/metadata-images.mjs +3 -3
- package/build/metadata-routes.mjs +3 -3
- package/build/native-packages.mjs +5 -1
- package/build/npm-packages.mjs +2 -1
- package/build/og.mjs +3 -9
- package/build/optimize-imports.mjs +112 -0
- package/build/output-contract.mjs +41 -0
- package/build/precompress.mjs +13 -0
- package/build/proxy-body-config.mjs +10 -0
- package/build/scan.mjs +10 -8
- package/build/static-adapter.mjs +100 -0
- package/cli.mjs +64 -11
- package/compat/after.cjs +51 -0
- package/compat/diagnostics.cjs +4 -0
- package/compat/index.d.ts +32 -1
- package/compat/server.cjs +2 -1
- package/compat/server.d.cts +2 -0
- package/index.mjs +2 -0
- package/native/launch.mjs +19 -0
- package/package.json +8 -9
- package/release/verify.mjs +2 -0
- package/runtime/api.mjs +20 -7
- package/runtime/app-render.mjs +78 -33
- package/runtime/app-route-plan.mjs +45 -0
- package/runtime/app-routing-state.mjs +101 -21
- package/runtime/app-routing.mjs +37 -18
- package/runtime/app-segments.mjs +25 -11
- package/runtime/app-static.mjs +1 -1
- package/runtime/app-stream-html.mjs +2 -1
- package/runtime/cache-request.mjs +10 -2
- package/runtime/cloud/archive.mjs +317 -0
- package/runtime/cloud/client.mjs +641 -0
- package/runtime/cloud/environment.mjs +60 -0
- package/runtime/cloud/state.mjs +156 -0
- package/runtime/cloud/upload.mjs +48 -0
- package/runtime/html-envelope.mjs +70 -7
- package/runtime/http.mjs +7 -4
- package/runtime/middleware.mjs +3 -3
- package/runtime/og/Geist-Regular.ttf +0 -0
- package/runtime/og/LICENSE +373 -0
- package/runtime/og/LICENSE.Geist +93 -0
- package/runtime/og/LICENSE.fflate +21 -0
- package/runtime/og/LICENSE.harfbuzzjs +22 -0
- package/runtime/og/LICENSE.satori +373 -0
- package/runtime/og/NOTICE.txt +4 -0
- package/runtime/og/hb.wasm +0 -0
- package/runtime/og/index.node.mjs +31260 -0
- package/runtime/og/resvg.wasm +0 -0
- package/runtime/og-backend.mjs +8 -0
- package/runtime/og-loader.mjs +2 -15
- package/runtime/request-transport.mjs +1 -1
- package/runtime/route-static.mjs +1 -1
- package/runtime/rsc-worker.mjs +27 -4
- package/runtime/stream-utils.mjs +22 -3
- package/runtime/transport.mjs +16 -0
- package/runtime/worker-metadata.mjs +23 -0
- package/runtime/worker.mjs +36 -9
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { mkdir, lstat, readFile, realpath, writeFile, rm, chmod } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
5
|
+
import { rename } from '../fs.mjs';
|
|
6
|
+
|
|
7
|
+
// Resolve existing parents too when a destination has not been created yet.
|
|
8
|
+
// On macOS, for example, /var and /private/var refer to the same directory.
|
|
9
|
+
export async function prospectivePath(value) {
|
|
10
|
+
let parent = path.resolve(value);
|
|
11
|
+
const missing = [];
|
|
12
|
+
for (;;) {
|
|
13
|
+
try { return path.join(await realpath(parent), ...missing); }
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error.code !== 'ENOENT' || path.dirname(parent) === parent) throw error;
|
|
16
|
+
missing.unshift(path.basename(parent));
|
|
17
|
+
parent = path.dirname(parent);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function cloudOrigin(value = 'https://app.prnext.dev') {
|
|
23
|
+
let url;
|
|
24
|
+
try {
|
|
25
|
+
url = new URL(value);
|
|
26
|
+
} catch {
|
|
27
|
+
throw new Error(
|
|
28
|
+
'Cloud server must be an HTTPS origin or a loopback HTTP origin.'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
if (
|
|
32
|
+
url.username ||
|
|
33
|
+
url.password ||
|
|
34
|
+
url.pathname !== '/' ||
|
|
35
|
+
url.search ||
|
|
36
|
+
url.hash ||
|
|
37
|
+
!(
|
|
38
|
+
url.protocol === 'https:' ||
|
|
39
|
+
(url.protocol === 'http:' &&
|
|
40
|
+
['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
throw new Error(
|
|
44
|
+
'Cloud server must be an HTTPS origin or a loopback HTTP origin.'
|
|
45
|
+
);
|
|
46
|
+
return url.origin;
|
|
47
|
+
}
|
|
48
|
+
export async function readState(filename) {
|
|
49
|
+
try {
|
|
50
|
+
const info = await lstat(filename);
|
|
51
|
+
if (!info.isFile() || info.isSymbolicLink() || info.size > 65536)
|
|
52
|
+
throw new Error('Invalid PRNext Cloud state file.');
|
|
53
|
+
const value = JSON.parse(await readFile(filename, 'utf8'));
|
|
54
|
+
if (!value || Array.isArray(value) || typeof value !== 'object')
|
|
55
|
+
throw new Error('Invalid PRNext Cloud state file.');
|
|
56
|
+
return value;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error.code === 'ENOENT') return null;
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export async function writeState(filename, data) {
|
|
63
|
+
const directory = path.dirname(filename);
|
|
64
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
65
|
+
const info = await lstat(directory);
|
|
66
|
+
if (info.isSymbolicLink() || !info.isDirectory())
|
|
67
|
+
throw new Error('PRNext Cloud state directory cannot be a link.');
|
|
68
|
+
await readState(filename);
|
|
69
|
+
if (process.platform !== 'win32') await chmod(directory, 0o700);
|
|
70
|
+
const temporary = `${filename}.${randomBytes(8).toString('hex')}.tmp`;
|
|
71
|
+
try {
|
|
72
|
+
await writeFile(temporary, JSON.stringify(data, null, 2) + '\n', {
|
|
73
|
+
flag: 'wx',
|
|
74
|
+
mode: 0o600
|
|
75
|
+
});
|
|
76
|
+
await rename(temporary, filename);
|
|
77
|
+
} finally {
|
|
78
|
+
await rm(temporary, { force: true });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export function credentialPath(origin, env = process.env) {
|
|
82
|
+
const directory = path.resolve(
|
|
83
|
+
env.PRNEXT_CLOUD_HOME || path.join(homedir(), '.prnext', 'cloud')
|
|
84
|
+
);
|
|
85
|
+
return path.join(
|
|
86
|
+
directory,
|
|
87
|
+
createHash('sha256').update(cloudOrigin(origin)).digest('hex') + '.json'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
export const linkPath = (directory) =>
|
|
91
|
+
path.join(directory, '.prnext-cloud', 'project.json');
|
|
92
|
+
export async function withStateLock(filename, task) {
|
|
93
|
+
await mkdir(path.dirname(filename), { recursive: true, mode: 0o700 });
|
|
94
|
+
if ((await lstat(path.dirname(filename))).isSymbolicLink())
|
|
95
|
+
throw new Error('Cloud state directory cannot be a link.');
|
|
96
|
+
const marker = { pid: process.pid, nonce: randomBytes(16).toString('hex') };
|
|
97
|
+
for (let attempt = 0; ; attempt++) {
|
|
98
|
+
try {
|
|
99
|
+
await writeFile(filename, JSON.stringify(marker), {
|
|
100
|
+
flag: 'wx',
|
|
101
|
+
mode: 0o600
|
|
102
|
+
});
|
|
103
|
+
break;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error.code !== 'EEXIST') throw error;
|
|
106
|
+
const previous = await readState(filename);
|
|
107
|
+
let active = true;
|
|
108
|
+
if (Number.isSafeInteger(previous?.pid) && previous.pid > 0) {
|
|
109
|
+
try {
|
|
110
|
+
process.kill(previous.pid, 0);
|
|
111
|
+
} catch (failure) {
|
|
112
|
+
if (failure.code === 'ESRCH') active = false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (active || attempt > 0)
|
|
116
|
+
throw new Error(
|
|
117
|
+
'Another PRNext Cloud command is using this project. Wait for it to finish.'
|
|
118
|
+
);
|
|
119
|
+
if ((await readState(filename))?.nonce === previous.nonce)
|
|
120
|
+
await rm(filename);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
return await task();
|
|
125
|
+
} finally {
|
|
126
|
+
if ((await readState(filename))?.nonce === marker.nonce) await rm(filename);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export async function saveLink(directory, link) {
|
|
130
|
+
await writeState(linkPath(directory), link);
|
|
131
|
+
const gitignore = path.join(directory, '.gitignore');
|
|
132
|
+
let contents = '';
|
|
133
|
+
try {
|
|
134
|
+
const stat = await lstat(gitignore);
|
|
135
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 65536)
|
|
136
|
+
throw new Error(
|
|
137
|
+
'Cannot safely update .gitignore; ignore .prnext-cloud/ manually.'
|
|
138
|
+
);
|
|
139
|
+
contents = await readFile(gitignore, 'utf8');
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (error.code !== 'ENOENT') throw error;
|
|
142
|
+
}
|
|
143
|
+
if (
|
|
144
|
+
!contents
|
|
145
|
+
.split(/\r?\n/)
|
|
146
|
+
.some((line) =>
|
|
147
|
+
['.prnext-cloud/', '/.prnext-cloud/', '.prnext-cloud'].includes(line)
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
await writeFile(
|
|
151
|
+
gitignore,
|
|
152
|
+
contents +
|
|
153
|
+
(contents.endsWith('\n') || !contents ? '' : '\n') +
|
|
154
|
+
'.prnext-cloud/\n'
|
|
155
|
+
);
|
|
156
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { open } from 'node:fs/promises';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
4
|
+
|
|
5
|
+
// The checkpoint contains identifiers/checksums only, never source or env contents.
|
|
6
|
+
export async function uploadSource(api, link, archive, {save, signal, log = () => {}}) {
|
|
7
|
+
const base = `/projects/${encodeURIComponent(link.projectId)}/uploads`;
|
|
8
|
+
const retry = async fn => {
|
|
9
|
+
for (let attempt=0;;attempt++) {
|
|
10
|
+
signal?.throwIfAborted();
|
|
11
|
+
try { return await fn(); }
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (signal?.aborted || attempt >= 3 || error.status && ![408,429,500,502,503,504].includes(error.status) && error.code !== 'upload_busy') throw error;
|
|
14
|
+
await delay(500 * 2**attempt,undefined,{signal});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
if (link.upload && link.upload.digest !== archive.digest) {
|
|
19
|
+
await api(`${base}/${link.upload.id}`,{method:'DELETE'}).catch(error => {if (![404,410].includes(error.status)) throw error;});
|
|
20
|
+
delete link.upload;
|
|
21
|
+
}
|
|
22
|
+
link.upload ||= {id:randomUUID(),digest:archive.digest};
|
|
23
|
+
await save(link);
|
|
24
|
+
let session;
|
|
25
|
+
try {
|
|
26
|
+
session = await retry(() => api(base,{method:'POST',body:{requestId:link.upload.id,bytes:archive.size,sourceBytes:archive.expandedBytes,digest:archive.digest}}));
|
|
27
|
+
} catch(error) {
|
|
28
|
+
if ([404,410].includes(error.status)) {delete link.upload;await save(link);}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
if (session.result) return session.result;
|
|
32
|
+
if (!Number.isSafeInteger(session.chunkBytes) || session.chunkBytes < 1 || session.chunkBytes > 4*1048576) throw new Error('Unsupported server upload chunk size.');
|
|
33
|
+
const parts = new Map(session.parts.map(p => [p.part,p.digest]));
|
|
34
|
+
const handle = await open(archive.path,'r');
|
|
35
|
+
try {
|
|
36
|
+
for (let offset=0,part=0;offset<archive.size;offset+=session.chunkBytes,part++) {
|
|
37
|
+
signal?.throwIfAborted();
|
|
38
|
+
const body = Buffer.alloc(Math.min(session.chunkBytes,archive.size-offset));
|
|
39
|
+
let read=0;
|
|
40
|
+
while (read<body.length) {const result=await handle.read(body,read,body.length-read,offset+read);if(!result.bytesRead)throw new Error('Temporary archive changed. Run the command again.');read+=result.bytesRead;}
|
|
41
|
+
const digest = createHash('sha256').update(body).digest('hex');
|
|
42
|
+
if (parts.has(part) && parts.get(part) !== digest) throw new Error('Resumed upload checksum differs. Cancel this upload and try again.');
|
|
43
|
+
if (!parts.has(part)) await retry(() => api(`${base}/${link.upload.id}/chunks/${part}`,{method:'PUT',body}));
|
|
44
|
+
log(`Uploading sources: ${Math.round(100*(offset+body.length)/archive.size)}%`);
|
|
45
|
+
}
|
|
46
|
+
} finally { await handle.close(); }
|
|
47
|
+
return retry(() => api(`${base}/${link.upload.id}/complete`,{method:'POST',body:{}}));
|
|
48
|
+
}
|
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
import { MAX_RESPONSE_BYTES } from './http.mjs';
|
|
2
2
|
|
|
3
|
+
// A raw element cannot be interrupted with a Flight script. Keep its chunks
|
|
4
|
+
// until its closing tag arrives, but only scan newly received characters.
|
|
5
|
+
// Joining and rescanning a growing inline script on every chunk is quadratic.
|
|
6
|
+
const whitespace = /\s/;
|
|
7
|
+
function holdRaw(state, text) {
|
|
8
|
+
if (!text) return;
|
|
9
|
+
// Compact small chunks in tiers: even single-character input cannot retain
|
|
10
|
+
// millions of array entries. Each tier joins once per 64 chunks, rather
|
|
11
|
+
// than repeatedly copying the entire accumulated element.
|
|
12
|
+
for (let level = 0;; level++) {
|
|
13
|
+
const parts = state.parts[level] ??= [];
|
|
14
|
+
parts.push(text);
|
|
15
|
+
if (parts.length < 64) return;
|
|
16
|
+
text = parts.join('');
|
|
17
|
+
parts.length = 0;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function releaseRaw(state) {
|
|
21
|
+
const parts = [];
|
|
22
|
+
for (let level = state.parts.length - 1; level >= 0; level--) parts.push(...state.parts[level]);
|
|
23
|
+
return parts.join('');
|
|
24
|
+
}
|
|
25
|
+
function rawEnd(text, state, from = 0) {
|
|
26
|
+
let index = from;
|
|
27
|
+
while (index < text.length) {
|
|
28
|
+
if (!state.matched) {
|
|
29
|
+
const start = text.indexOf('<', index);
|
|
30
|
+
if (start < 0) return -1;
|
|
31
|
+
state.matched = 1;
|
|
32
|
+
index = start + 1;
|
|
33
|
+
} else if (state.matched < state.prefix.length) {
|
|
34
|
+
const code = text.charCodeAt(index);
|
|
35
|
+
if (state.matched === 1 ? code === 47 : (code | 32) === state.prefix.charCodeAt(state.matched)) {
|
|
36
|
+
state.matched++;
|
|
37
|
+
index++;
|
|
38
|
+
} else state.matched = 0;
|
|
39
|
+
} else if (text[index] === '>') {
|
|
40
|
+
return index + 1;
|
|
41
|
+
} else if (whitespace.test(text[index])) {
|
|
42
|
+
index++;
|
|
43
|
+
} else state.matched = 0;
|
|
44
|
+
}
|
|
45
|
+
return -1;
|
|
46
|
+
}
|
|
47
|
+
|
|
3
48
|
// Do not insert Flight/bootstrap scripts inside a split tag or raw-text element.
|
|
4
49
|
// Scan by index: ordinary tags stay in one contiguous slice, instead of creating
|
|
5
50
|
// a new tail and output string for every cell of a rendered table.
|
|
@@ -7,6 +52,7 @@ export function htmlEnvelope({ resumed = false } = {}) {
|
|
|
7
52
|
let pending = '';
|
|
8
53
|
let closing = '';
|
|
9
54
|
let received = 0;
|
|
55
|
+
let rawPending;
|
|
10
56
|
const rawStart = /<(script|style|textarea|title|svg|math)(?:\s|>)/iy;
|
|
11
57
|
const rawEnds = new Map();
|
|
12
58
|
const documentEnd = /<\/(body|html)\s*>/iy;
|
|
@@ -14,8 +60,17 @@ export function htmlEnvelope({ resumed = false } = {}) {
|
|
|
14
60
|
append(text) {
|
|
15
61
|
received += Buffer.byteLength(text);
|
|
16
62
|
if (received > MAX_RESPONSE_BYTES) throw new Error('HTML response exceeds the 16 MiB PRNext limit');
|
|
63
|
+
let ready = '';
|
|
64
|
+
if (rawPending) {
|
|
65
|
+
const end = rawEnd(text, rawPending);
|
|
66
|
+
if (end < 0) { holdRaw(rawPending, text); return ''; }
|
|
67
|
+
holdRaw(rawPending, text.slice(0, end));
|
|
68
|
+
ready = releaseRaw(rawPending);
|
|
69
|
+
rawPending = undefined;
|
|
70
|
+
text = text.slice(end);
|
|
71
|
+
}
|
|
17
72
|
pending += text;
|
|
18
|
-
let cursor = 0, output = 0
|
|
73
|
+
let cursor = 0, output = 0;
|
|
19
74
|
while (cursor < pending.length) {
|
|
20
75
|
const start = pending.indexOf('<', cursor);
|
|
21
76
|
if (start < 0) { cursor = pending.length; break; }
|
|
@@ -25,12 +80,13 @@ export function htmlEnvelope({ resumed = false } = {}) {
|
|
|
25
80
|
const comment = pending.indexOf('-->', start);
|
|
26
81
|
if (comment >= 0) end = comment + 2;
|
|
27
82
|
} else {
|
|
28
|
-
let quote;
|
|
29
83
|
for (let index = start + 1; index < pending.length; index++) {
|
|
30
84
|
const char = pending[index];
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
85
|
+
if (char === '"' || char === "'") {
|
|
86
|
+
const close = pending.indexOf(char, index + 1);
|
|
87
|
+
if (close < 0) break;
|
|
88
|
+
index = close;
|
|
89
|
+
} else if (char === '>') { end = index; break; }
|
|
34
90
|
}
|
|
35
91
|
}
|
|
36
92
|
if (end < 0) break;
|
|
@@ -49,7 +105,14 @@ export function htmlEnvelope({ resumed = false } = {}) {
|
|
|
49
105
|
if (!pattern) { pattern = new RegExp(`</${name}\\s*>`, 'ig'); rawEnds.set(name, pattern); }
|
|
50
106
|
pattern.lastIndex = end + 1;
|
|
51
107
|
const match = pattern.exec(pending);
|
|
52
|
-
if (!match)
|
|
108
|
+
if (!match) {
|
|
109
|
+
rawPending = { prefix: `</${name}`, matched: 0, parts: [] };
|
|
110
|
+
holdRaw(rawPending, pending.slice(start));
|
|
111
|
+
rawEnd(pending, rawPending, end + 1);
|
|
112
|
+
ready += pending.slice(output, start);
|
|
113
|
+
pending = '';
|
|
114
|
+
return ready;
|
|
115
|
+
}
|
|
53
116
|
cursor = pattern.lastIndex;
|
|
54
117
|
} else {
|
|
55
118
|
documentEnd.lastIndex = start;
|
|
@@ -66,7 +129,7 @@ export function htmlEnvelope({ resumed = false } = {}) {
|
|
|
66
129
|
return ready;
|
|
67
130
|
},
|
|
68
131
|
finish() {
|
|
69
|
-
if (pending || !(resumed ? /^(?:<\/body><\/html>){1,2}$/i : /^<\/body><\/html>$/i).test(closing)) throw new Error('App Router root layout did not close its body and html elements');
|
|
132
|
+
if (pending || rawPending || !(resumed ? /^(?:<\/body><\/html>){1,2}$/i : /^<\/body><\/html>$/i).test(closing)) throw new Error('App Router root layout did not close its body and html elements');
|
|
70
133
|
return resumed ? '</body></html>' : closing;
|
|
71
134
|
},
|
|
72
135
|
};
|
package/runtime/http.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
// ESM node:http evaluates lazy WebSocket exports and initializes Undici.
|
|
3
|
+
// Use the module object to avoid that startup cost while preserving HTTP patches.
|
|
4
|
+
const http = createRequire(import.meta.url)('node:http');
|
|
2
5
|
import { Duplex, Writable } from 'node:stream';
|
|
3
6
|
import { revalidatePage } from './pages-revalidate.mjs';
|
|
4
7
|
import { currentRequest } from '../compat/headers.cjs';
|
|
@@ -45,7 +48,7 @@ export function createRequest({ url, originalUrl, method = 'GET', headers = {},
|
|
|
45
48
|
const visible = originalUrl ? new URL(originalUrl, parsed) : parsed;
|
|
46
49
|
const socket = new Duplex({ read() {}, write(_chunk, _encoding, callback) { callback(); } });
|
|
47
50
|
socket.remoteAddress = '127.0.0.1';
|
|
48
|
-
const request = new IncomingMessage(socket);
|
|
51
|
+
const request = new http.IncomingMessage(socket);
|
|
49
52
|
request.method = method;
|
|
50
53
|
request.url = `${visible.pathname}${visible.search}`;
|
|
51
54
|
request.headers = Object.fromEntries(Object.entries(headers).map(([name, value]) => [name.toLowerCase(), value]));
|
|
@@ -85,9 +88,9 @@ export class CapturedResponse extends Writable {
|
|
|
85
88
|
get finished() { return this.writableEnded; }
|
|
86
89
|
setHeader(name, value) {
|
|
87
90
|
if (this.headersSent) throw new Error('Cannot set headers after they are sent');
|
|
88
|
-
validateHeaderName(name);
|
|
91
|
+
http.validateHeaderName(name);
|
|
89
92
|
const normalized = Array.isArray(value) ? value.map(String) : String(value);
|
|
90
|
-
for (const item of Array.isArray(normalized) ? normalized : [normalized]) validateHeaderValue(name, item);
|
|
93
|
+
for (const item of Array.isArray(normalized) ? normalized : [normalized]) http.validateHeaderValue(name, item);
|
|
91
94
|
this._headers[name.toLowerCase()] = normalized;
|
|
92
95
|
return this;
|
|
93
96
|
}
|
package/runtime/middleware.mjs
CHANGED
|
@@ -12,7 +12,7 @@ installFetchCache();
|
|
|
12
12
|
|
|
13
13
|
const FLIGHT_HEADERS = ['rsc', 'next-router-state-tree', 'x-prnext-router-state', 'next-router-prefetch', 'next-hmr-refresh', 'next-router-segment-prefetch'];
|
|
14
14
|
const HOP_HEADERS = new Set(['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade', 'content-length']);
|
|
15
|
-
const MAX_BODY =
|
|
15
|
+
const MAX_BODY = 32 * 1024 * 1024;
|
|
16
16
|
const MAX_SCOPES = 32;
|
|
17
17
|
const MAX_PROMISES = 128;
|
|
18
18
|
const scopes = new Set();
|
|
@@ -133,9 +133,9 @@ export async function runMiddleware(options) {
|
|
|
133
133
|
if (typeof handler !== 'function') throw new TypeError(`Middleware must export a ${exportName} function`);
|
|
134
134
|
const init = { method: options.method || 'GET', headers, signal: controller.signal };
|
|
135
135
|
if (!['GET', 'HEAD'].includes(init.method)) {
|
|
136
|
-
if (typeof options.body === 'string' && options.body.length > Math.ceil(MAX_BODY / 3) * 4) throw Object.assign(new Error('Middleware request exceeds the
|
|
136
|
+
if (typeof options.body === 'string' && options.body.length > Math.ceil(MAX_BODY / 3) * 4) throw Object.assign(new Error('Middleware request exceeds the 32 MiB PRNext limit'), { statusCode: 413 });
|
|
137
137
|
const body = Buffer.isBuffer(options.body) ? options.body : Buffer.from(options.body || '', 'base64');
|
|
138
|
-
if (body.byteLength > MAX_BODY) throw Object.assign(new Error('Middleware request exceeds the
|
|
138
|
+
if (body.byteLength > MAX_BODY) throw Object.assign(new Error('Middleware request exceeds the 32 MiB PRNext limit'), { statusCode: 413 });
|
|
139
139
|
if (body.byteLength) init.body = body;
|
|
140
140
|
}
|
|
141
141
|
const request = new NextRequest(exposedUrl, { ...init, nextConfig: options.manifest?.config });
|
|
Binary file
|