@thomas.f/prnext 0.1.19-alpha → 0.1.20-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/build/app.mjs +46 -78
- package/build/build-lock.mjs +2 -71
- package/build/build-retention.mjs +119 -0
- package/build/bundle-runtime.mjs +9 -8
- package/build/cache-command.mjs +31 -0
- package/build/check.mjs +41 -3
- package/build/compilation-cache.mjs +32 -12
- package/build/compilation-storage.mjs +190 -0
- package/build/compiler.mjs +3 -3
- package/build/config.mjs +23 -2
- package/build/create.mjs +1 -1
- package/build/css-modules.mjs +4 -0
- package/build/dev-action-graph.mjs +21 -0
- package/build/font-cache.mjs +9 -6
- package/build/font-files.mjs +1 -1
- package/build/index.mjs +30 -13
- package/build/native-ppr.mjs +32 -0
- package/build/og.mjs +121 -1
- package/build/parallel-plan.mjs +55 -0
- package/build/persistent-cache.mjs +29 -24
- package/build/production-check.mjs +34 -0
- package/build/react-compiler-report.mjs +31 -8
- package/build/react-compiler.mjs +101 -10
- package/build/server-config.mjs +35 -0
- package/build/source-analysis.mjs +54 -4
- package/build/static-adapter.mjs +7 -3
- package/build/transform.mjs +38 -6
- package/build/typescript-dev.mjs +3 -1
- package/cli.mjs +14 -5
- package/compat/app-layout-context.cjs +7 -3
- package/compat/flight-delta.cjs +47 -12
- package/compat/index.d.ts +24 -1
- package/compat/navigation-authorization.cjs +8 -1
- package/docs/ai/compatibility.md +110 -7
- package/docs/ai/deployment.md +141 -0
- package/docs/ai/quickstart.md +5 -0
- package/package.json +12 -9
- package/release/fontkit.mjs +31 -0
- package/release/verify.mjs +3 -0
- package/runtime/api.mjs +5 -2
- package/runtime/app-client.mjs +150 -26
- package/runtime/app-partial.mjs +2 -10
- package/runtime/app-prefetch.mjs +2 -2
- package/runtime/app-render.mjs +11 -7
- package/runtime/app-stream-channel.mjs +3 -2
- package/runtime/app-styles.mjs +22 -4
- package/runtime/compile-cache-retention.mjs +53 -0
- package/runtime/compile-cache.mjs +28 -1
- package/runtime/dev-client.mjs +6 -3
- package/runtime/dev-gateway.mjs +3 -2
- package/runtime/dev-route-selection.mjs +64 -0
- package/runtime/dev-runtime-pool.mjs +3 -2
- package/runtime/dev-runtime-spare.mjs +1 -1
- package/runtime/dev-session-retention.mjs +154 -0
- package/runtime/development.mjs +27 -8
- package/runtime/env.mjs +9 -0
- package/runtime/flight-delta.mjs +31 -5
- package/runtime/native-ppr.mjs +35 -0
- package/runtime/og/NOTICE.txt +1 -1
- package/runtime/og/index.node.mjs +76 -31216
- package/runtime/og/upstream.json +115 -0
- package/runtime/persistent/client.mjs +4 -2
- package/runtime/persistent/daemon.mjs +3 -11
- package/runtime/persistent/health.mjs +58 -0
- package/runtime/project-lock.mjs +71 -0
- package/runtime/request-transport.mjs +112 -32
- package/runtime/storage-path.mjs +36 -0
- package/runtime/trace.mjs +44 -4
- package/runtime/worker.mjs +9 -4
- package/vendor/fontkit/NOTICE.txt +5 -0
- package/vendor/fontkit/SOURCE-NOTICES.txt +209 -0
- package/vendor/fontkit/index.mjs +3 -0
- package/vendor/fontkit/licenses/@swc__helpers/LICENSE +201 -0
- package/vendor/fontkit/licenses/base64-js/LICENSE +21 -0
- package/vendor/fontkit/licenses/brotli/readme.md +61 -0
- package/vendor/fontkit/licenses/clone/LICENSE +18 -0
- package/vendor/fontkit/licenses/dfa/README.md +83 -0
- package/vendor/fontkit/licenses/fast-deep-equal/LICENSE +21 -0
- package/vendor/fontkit/licenses/fontkit/README.md +266 -0
- package/vendor/fontkit/licenses/restructure/LICENSE +21 -0
- package/vendor/fontkit/licenses/tiny-inflate/LICENSE +21 -0
- package/vendor/fontkit/licenses/tslib/LICENSE.txt +12 -0
- package/vendor/fontkit/licenses/unicode-properties/LICENSE +8 -0
- package/vendor/fontkit/licenses/unicode-trie/LICENSE +8 -0
- package/vendor/fontkit/upstream.json +215 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"name": "@vercel/og",
|
|
4
|
+
"inputs": {
|
|
5
|
+
"sourceSha256": "3269eb15ca135a0eb7e07ddf8af0b35711d6e75c56b84c6a6727c5626d59409c",
|
|
6
|
+
"packages": {
|
|
7
|
+
"@vercel/og": {
|
|
8
|
+
"version": "1.0.3",
|
|
9
|
+
"integrity": "sha512-/iYJ34PDu4lty1TXdL86qHEw8viveHTllG1PWLuJX8tSFfqKL7kyVJeUORJmQ250bHB0h+alcalXUlza3c/NSA=="
|
|
10
|
+
},
|
|
11
|
+
"fflate": {
|
|
12
|
+
"version": "0.7.5",
|
|
13
|
+
"integrity": "sha512-QieYf//cis6ywHNi5qW1+PXPQ4bC+XVJAtS4AXIML8P76GroEiOxm/oQtn1f02UkJY1+KsXMJcC+R2v/Eg4G3g=="
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"components": [
|
|
17
|
+
{
|
|
18
|
+
"name": "@vercel/og",
|
|
19
|
+
"version": "1.0.3"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "@resvg/resvg-wasm",
|
|
23
|
+
"version": "2.4.1"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "@shuding/opentype.js",
|
|
27
|
+
"version": "1.4.0-beta.0"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "base64-js",
|
|
31
|
+
"version": "0.0.8"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "camelize",
|
|
35
|
+
"version": "1.0.1"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "color-name",
|
|
39
|
+
"version": "1.1.4"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "css-background-parser",
|
|
43
|
+
"version": "0.1.0"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "css-box-shadow",
|
|
47
|
+
"version": "1.0.0-3"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "css-color-keywords",
|
|
51
|
+
"version": "1.0.0"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "css-gradient-parser",
|
|
55
|
+
"version": "0.0.17"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "css-to-react-native",
|
|
59
|
+
"version": "3.2.0"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "escape-html",
|
|
63
|
+
"version": "1.0.3"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "fflate",
|
|
67
|
+
"version": "0.7.5"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "harfbuzzjs",
|
|
71
|
+
"version": "0.10.0"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "hex-rgb",
|
|
75
|
+
"version": "4.3.0"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"name": "linebreak",
|
|
79
|
+
"version": "1.1.0"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"name": "parse-css-color",
|
|
83
|
+
"version": "0.2.1"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "postcss-value-parser",
|
|
87
|
+
"version": "4.2.0"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"name": "satori",
|
|
91
|
+
"version": "0.33.5"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"name": "tiny-inflate",
|
|
95
|
+
"version": "1.0.3"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "unicode-trie",
|
|
99
|
+
"version": "2.0.0"
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
"files": {
|
|
104
|
+
"Geist-Regular.ttf": "bde046ddd9f20be35b0bd56cc79eb752b967fb6661a3fe76cb067bb09f871d76",
|
|
105
|
+
"hb.wasm": "d2ab975172d6103e7624e8f7f03fea2316eca6fe562e1b80fc7f7058b283a6d0",
|
|
106
|
+
"index.node.mjs": "4fae6c6a5e3b6514b6a303bb2f9f5c945114aed0549724191a8ef127238e075b",
|
|
107
|
+
"LICENSE": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5",
|
|
108
|
+
"LICENSE.fflate": "62249119bb7df6e54300f37a37145c26a6d4420410aac8881a119c5b427cf751",
|
|
109
|
+
"LICENSE.Geist": "c683bfbcc7e087f5d37a54ef628f10387c451a83ddc459b151403a164ac46c90",
|
|
110
|
+
"LICENSE.harfbuzzjs": "c85596583d1b7e4cfb4f21165ce0988aabd72e80a8c3a2ab6aaeedc1965e8288",
|
|
111
|
+
"LICENSE.satori": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5",
|
|
112
|
+
"NOTICE.txt": "bcc9a8f06053c81502b72a618ca004794027ec85ae94d4bbe81890f7dcdb8ec8",
|
|
113
|
+
"resvg.wasm": "9c1f8f54b842fcda3b72b06eba94a20b907958c443d73178da32b21ccb57b9bb"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { initializeHome, rpc, delay, environment, validName, validHealthPath } from './common.mjs';
|
|
6
6
|
import { resolveNativeBinary } from '../../native/resolve.mjs';
|
|
7
|
+
import {nativeHealthPath} from './health.mjs';
|
|
7
8
|
|
|
8
9
|
export const persistentHelp = `Persistent apps (ordinary prn start stays in the foreground):
|
|
9
10
|
prn pstart [directory] [options] Start a built app in the background
|
|
@@ -18,7 +19,8 @@ export const persistentHelp = `Persistent apps (ordinary prn start stays in the
|
|
|
18
19
|
|
|
19
20
|
pstart options: --name NAME --port 3000 --hostname 127.0.0.1 --workers 1
|
|
20
21
|
--profile balanced|speed|memory|classic --inspect
|
|
21
|
-
--health-path /
|
|
22
|
+
--health-path /api/health (optional application probe after the native readiness check)
|
|
23
|
+
--health-timeout 15000 --drain-timeout 30000 (milliseconds)
|
|
22
24
|
--rollback-window 15000 (post-switch monitoring; 0 disables, maximum 120000 ms)
|
|
23
25
|
prestart/pstop/plogs default to the app in the current directory.
|
|
24
26
|
plogs options: --lines 100 --follow. App environment comes from its .env files.
|
|
@@ -157,7 +159,7 @@ export async function persistentCommand(command, argv) {
|
|
|
157
159
|
if (command === 'pstart') {
|
|
158
160
|
const root = await realpath(path.resolve(target || '.'));
|
|
159
161
|
const name = validName(options.name || path.basename(root).replace(/[^a-zA-Z0-9_-]/g, '-').replace(/^[^a-zA-Z0-9]+/, '').slice(0, 64));
|
|
160
|
-
request = { command: 'start', config: { name, root, binary: await resolveNativeBinary(), node: process.execPath, port: options.port || 3000, hostname: options.hostname || '127.0.0.1', workers: options.workers || 1, profile: options.profile || process.env.PRNEXT_PROFILE || 'balanced', inspect: !!options.inspect, healthPath: options['health-path'] ||
|
|
162
|
+
request = { command: 'start', config: { name, root, binary: await resolveNativeBinary(), node: process.execPath, port: options.port || 3000, hostname: options.hostname || '127.0.0.1', workers: options.workers || 1, profile: options.profile || process.env.PRNEXT_PROFILE || 'balanced', inspect: !!options.inspect, healthPath: options['health-path'] || nativeHealthPath, healthTimeout: options['health-timeout'] || 15000, drainTimeout: options['drain-timeout'] || 30000, rollbackWindow: options['rollback-window'] ?? 15000 } };
|
|
161
163
|
} else request = { command: ({ prestart: 'restart', pstop: 'stop', pdelete: 'delete' })[command], name: target, all: !!options.all, root: target || options.all ? undefined : await realpath(process.cwd()) };
|
|
162
164
|
await ensure(connection);
|
|
163
165
|
const report = await rpc(connection, request);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { request } from 'node:http';
|
|
3
2
|
import { cp, mkdir, readFile, rm, realpath } from 'node:fs/promises';
|
|
4
3
|
import { randomUUID, createHash } from 'node:crypto';
|
|
5
4
|
import path from 'node:path';
|
|
@@ -7,6 +6,8 @@ import { readBuildDirectory } from '../build-directory.mjs';
|
|
|
7
6
|
import { atomicJSON, delay, environment, validName, validHealthPath } from './common.mjs';
|
|
8
7
|
import { RotatingLog } from './logger.mjs';
|
|
9
8
|
import {nativeGateway,stopNativeProxy} from './native-proxy.mjs';
|
|
9
|
+
import {health} from './health.mjs';
|
|
10
|
+
import {productionEnvironment} from '../env.mjs';
|
|
10
11
|
|
|
11
12
|
const home = process.argv[2];
|
|
12
13
|
const registry = path.join(home, 'apps.json');
|
|
@@ -78,15 +79,6 @@ async function snapshot(app) {
|
|
|
78
79
|
return { relative, buildId: manifest.buildId || null };
|
|
79
80
|
} catch (error) { await removeSnapshot(app, relative); throw error; }
|
|
80
81
|
}
|
|
81
|
-
function health(generation, config, timeout) {
|
|
82
|
-
return new Promise((resolve, reject) => {
|
|
83
|
-
const req = request({ host: '127.0.0.1', port: generation.port, path: config.healthPath, method: 'GET', headers: { host: `${config.hostname.includes(':') ? '[' + config.hostname + ']' : config.hostname}:${config.port}` }, signal: AbortSignal.timeout(timeout) }, res => {
|
|
84
|
-
const ok = res.statusCode >= 200 && res.statusCode < 400;
|
|
85
|
-
res.destroy(); ok ? resolve() : reject(new Error(`Health check ${config.healthPath} returned HTTP ${res.statusCode}`));
|
|
86
|
-
});
|
|
87
|
-
req.once('error', reject); req.end();
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
82
|
async function stopGeneration(app, generation, drain = true) {
|
|
91
83
|
if (generation.stopping) return generation.stopping;
|
|
92
84
|
generation.expected = true;
|
|
@@ -123,7 +115,7 @@ async function launch(app) {
|
|
|
123
115
|
const build = await snapshot(app);
|
|
124
116
|
const child = spawn(config.binary, ['start', config.root, '--build-dir', build.relative, '--hostname', '127.0.0.1', '--port', '0', '--node', config.node, '--workers', String(config.workers), '--profile', config.profile, '--shutdown-on-stdin-eof'], {
|
|
125
117
|
cwd: config.root, windowsHide: true, detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'],
|
|
126
|
-
env: { ...environment(),
|
|
118
|
+
env: { ...productionEnvironment(config.root,{env:{...environment(),NODE_ENV:'production'}}), ...(config.inspect ? { PRNEXT_INSPECT_DIR: path.join(config.root, '.prnext-cache/inspect') } : {}) },
|
|
127
119
|
});
|
|
128
120
|
child.stdin.on('error', () => {});
|
|
129
121
|
const generation = { child, ...build, active: 0, dead: false, expected: false, port: null, startedAt: Date.now() };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {request} from 'node:http';
|
|
2
|
+
|
|
3
|
+
export const nativeHealthPath = '/_prnext/health';
|
|
4
|
+
const nativeLimit = 4096, applicationLimit = 65536;
|
|
5
|
+
|
|
6
|
+
// Connect directly to the private generation. A redirect, partial response or
|
|
7
|
+
// open-ended body cannot certify that a process is ready to receive traffic.
|
|
8
|
+
function probe(generation, config, healthPath, timeout, native) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
let finished = false, response;
|
|
11
|
+
const finish = error => {
|
|
12
|
+
if (finished) return;
|
|
13
|
+
finished = true;
|
|
14
|
+
if (error) { response?.destroy(); req.destroy(); reject(error); }
|
|
15
|
+
else resolve();
|
|
16
|
+
};
|
|
17
|
+
const req = request({host:'127.0.0.1', port:generation.port, path:healthPath,
|
|
18
|
+
method:'GET', headers:{host:`${config.hostname.includes(':')?'['+config.hostname+']':config.hostname}:${config.port}`, accept:'application/json', 'accept-encoding':'identity', connection:'close'},
|
|
19
|
+
signal:AbortSignal.timeout(Math.max(1, timeout))}, res => {
|
|
20
|
+
response = res;
|
|
21
|
+
if (res.statusCode !== 200) return finish(new Error(`Health check ${healthPath} returned HTTP ${res.statusCode}`));
|
|
22
|
+
if (res.headers['content-encoding'] && res.headers['content-encoding'] !== 'identity') return finish(new Error(`Health check ${healthPath} returned an encoded body`));
|
|
23
|
+
const limit = native ? nativeLimit : applicationLimit;
|
|
24
|
+
const declared = res.headers['content-length'];
|
|
25
|
+
if (declared !== undefined && (!/^\d+$/.test(declared) || Number(declared) > limit)) return finish(new Error(`Health check ${healthPath} body exceeds ${limit} bytes`));
|
|
26
|
+
const chunks = []; let size = 0;
|
|
27
|
+
res.on('data', chunk => {
|
|
28
|
+
size += chunk.length;
|
|
29
|
+
if (size > limit) return finish(new Error(`Health check ${healthPath} body exceeds ${limit} bytes`));
|
|
30
|
+
if (native) chunks.push(chunk);
|
|
31
|
+
});
|
|
32
|
+
res.once('error', finish);
|
|
33
|
+
res.once('aborted', () => finish(new Error(`Health check ${healthPath} response was incomplete`)));
|
|
34
|
+
res.once('end', () => {
|
|
35
|
+
if (!res.complete) return finish(new Error(`Health check ${healthPath} response was incomplete`));
|
|
36
|
+
if (native) {
|
|
37
|
+
if (!/^application\/json(?:\s*;|$)/i.test(res.headers['content-type'] || '')) return finish(new Error(`Health check ${healthPath} must return application/json`));
|
|
38
|
+
let body;
|
|
39
|
+
try { body = JSON.parse(Buffer.concat(chunks, size).toString('utf8')); } catch { return finish(new Error(`Health check ${healthPath} returned invalid JSON`)); }
|
|
40
|
+
if (!body || Array.isArray(body) || body.schemaVersion !== 1 || body.status !== 'ready') return finish(new Error(`Health check ${healthPath} did not report ready`));
|
|
41
|
+
if (body.buildId !== undefined && generation.buildId && body.buildId !== generation.buildId) return finish(new Error(`Health check ${healthPath} returned a different build`));
|
|
42
|
+
}
|
|
43
|
+
finish();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
req.once('error', finish); req.end();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function health(generation, config, timeout) {
|
|
51
|
+
const deadline = Date.now() + timeout;
|
|
52
|
+
await probe(generation, config, nativeHealthPath, timeout, true);
|
|
53
|
+
if (config.healthPath !== nativeHealthPath) {
|
|
54
|
+
const remaining = deadline - Date.now();
|
|
55
|
+
if (remaining <= 0) throw new Error('Application health check exceeded its deadline');
|
|
56
|
+
await probe(generation, config, config.healthPath, remaining, false);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { mkdir, writeFile, rename, rm, realpath, lstat, open } from 'node:fs/promises';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { hostname } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const LOCK = '.prnext-build.lock';
|
|
7
|
+
const busy = message => Object.assign(new Error(message), { code: 'PRNEXT_BUILD_LOCKED' });
|
|
8
|
+
function alive(pid) {
|
|
9
|
+
try { process.kill(pid, 0); return true; }
|
|
10
|
+
catch (error) { return error.code !== 'ESRCH'; }
|
|
11
|
+
}
|
|
12
|
+
async function owner(directory) {
|
|
13
|
+
const info = await lstat(directory);
|
|
14
|
+
if (!info.isDirectory() || info.isSymbolicLink()) throw busy(`The project lock is not a real directory. Check ${path.basename(directory)} before retrying.`);
|
|
15
|
+
const file = await open(path.join(directory, 'owner.json'), 'r');
|
|
16
|
+
try {
|
|
17
|
+
if ((await file.stat()).size > 4096) throw new Error('oversized owner');
|
|
18
|
+
const value = JSON.parse(await file.readFile('utf8'));
|
|
19
|
+
if (!Number.isSafeInteger(value.pid) || value.pid < 1 || typeof value.host !== 'string' || !/^[a-f0-9-]{36}$/.test(value.token)) throw new Error('invalid owner');
|
|
20
|
+
return value;
|
|
21
|
+
} finally { await file.close(); }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** One publisher per canonical project, covering React and direct static imports. */
|
|
25
|
+
export async function withBuildLock(directory, action, {development = false} = {}) {
|
|
26
|
+
const root = await realpath(directory), lock = path.join(root, development ? '.prnext-dev.lock' : LOCK);
|
|
27
|
+
const identity = { pid: process.pid, host: hostname(), token: randomUUID(), createdAt: new Date().toISOString() };
|
|
28
|
+
const prepared = path.join(root, `.prnext-lock-${identity.token}`);
|
|
29
|
+
await mkdir(prepared, { mode: 0o700 });
|
|
30
|
+
try {
|
|
31
|
+
await writeFile(path.join(prepared, 'owner.json'), JSON.stringify(identity), { flag: 'wx', mode: 0o600 });
|
|
32
|
+
for (let attempt = 0; ; attempt++) {
|
|
33
|
+
try { await rename(prepared, lock); break; }
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (!['EEXIST', 'ENOTEMPTY', 'EPERM', 'EACCES'].includes(error.code)) throw error;
|
|
36
|
+
let previous;
|
|
37
|
+
try { previous = await owner(lock); }
|
|
38
|
+
catch (cause) {
|
|
39
|
+
if (cause.code === 'ENOENT' && attempt < 8) continue;
|
|
40
|
+
throw busy(`Cannot verify the project lock. Check ${path.basename(lock)} before retrying.`);
|
|
41
|
+
}
|
|
42
|
+
if (previous.host !== identity.host || alive(previous.pid)) throw busy(development ? `A development server is already running for this project (PID ${previous.pid}). Stop it before starting another.` : `A build is already running for this project (PID ${previous.pid}). Wait for it to finish.`);
|
|
43
|
+
// Exactly one contender can recover this generation. Never delete a lock
|
|
44
|
+
// merely because it is old: long builds and suspended processes are valid.
|
|
45
|
+
let claim;
|
|
46
|
+
try { claim = await open(path.join(lock, 'recovery'), 'wx', 0o600); }
|
|
47
|
+
catch (cause) {
|
|
48
|
+
if (cause.code === 'ENOENT' && attempt < 8) continue;
|
|
49
|
+
throw busy(`Project lock recovery is in progress. Retry shortly; if recovery was interrupted, check ${path.basename(lock)} manually.`);
|
|
50
|
+
}
|
|
51
|
+
const retired = path.join(root, `.prnext-lock-retired-${identity.token}`);
|
|
52
|
+
try {
|
|
53
|
+
await claim.writeFile(JSON.stringify(identity)); await claim.close(); claim = null;
|
|
54
|
+
const current = await owner(lock);
|
|
55
|
+
if (current.token !== previous.token) {
|
|
56
|
+
// Do not unlink through this path: the new owner can release and
|
|
57
|
+
// another generation can appear between our read and the unlink.
|
|
58
|
+
throw busy('The build lock changed during recovery. Retry the build.');
|
|
59
|
+
}
|
|
60
|
+
await rename(lock, retired);
|
|
61
|
+
await rm(retired, { recursive: true });
|
|
62
|
+
} finally { await claim?.close(); }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
try { return await action(root); }
|
|
66
|
+
finally {
|
|
67
|
+
// Ownership must still match before releasing a lock.
|
|
68
|
+
if ((await owner(lock)).token === identity.token) await rm(lock, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
} finally { await rm(prepared, { recursive: true, force: true }); }
|
|
71
|
+
}
|
|
@@ -1,41 +1,121 @@
|
|
|
1
1
|
const MAX_HEADER = 256 * 1024;
|
|
2
2
|
const MAX_BODY = 32 * 1024 * 1024;
|
|
3
|
+
export const REQUEST_BODY_CHUNK = 64 * 1024;
|
|
3
4
|
const EMPTY_BODY = Buffer.alloc(0);
|
|
5
|
+
export const requestBodyState = Symbol('PRNext request body state');
|
|
4
6
|
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
while (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
7
|
+
// One owner reads a lane. Streaming bodies borrow this reader until their
|
|
8
|
+
// invocation finishes; body bytes are never parsed as request metadata.
|
|
9
|
+
function laneReader(source) {
|
|
10
|
+
const iterator = source[Symbol.asyncIterator]?.() || source[Symbol.iterator]();
|
|
11
|
+
let chunk = EMPTY_BODY, offset = 0, ended = false;
|
|
12
|
+
async function available() {
|
|
13
|
+
while (offset === chunk.length && !ended) {
|
|
14
|
+
const next = await iterator.next(); ended = !!next.done;
|
|
15
|
+
chunk = ended ? EMPTY_BODY : next.value; offset = 0;
|
|
16
|
+
if (!Buffer.isBuffer(chunk)) chunk = Buffer.from(chunk);
|
|
17
|
+
}
|
|
18
|
+
return !ended;
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
async line(limit) {
|
|
22
|
+
const parts = []; let size = 0;
|
|
23
|
+
while (await available()) {
|
|
24
|
+
const end = chunk.indexOf(10, offset), stop = end < 0 ? chunk.length : end;
|
|
25
|
+
const part = chunk.subarray(offset, stop); size += part.length;
|
|
26
|
+
if (size > limit) throw new Error('Request transport header exceeds limit');
|
|
27
|
+
parts.push(part); offset = end < 0 ? stop : end + 1;
|
|
28
|
+
if (end >= 0) return (parts.length === 1 ? parts[0] : Buffer.concat(parts, size)).toString();
|
|
29
|
+
}
|
|
30
|
+
if (size) throw new Error('Truncated request transport header');
|
|
31
|
+
return undefined;
|
|
32
|
+
},
|
|
33
|
+
async exact(length) {
|
|
34
|
+
if (length === 0) return EMPTY_BODY;
|
|
35
|
+
const result = Buffer.allocUnsafe(length); let written = 0;
|
|
36
|
+
while (written < length) {
|
|
37
|
+
if (!await available()) throw new Error('Truncated request transport body');
|
|
38
|
+
const count = Math.min(length - written, chunk.length - offset);
|
|
39
|
+
chunk.copy(result, written, offset, offset + count); written += count; offset += count;
|
|
20
40
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
return result;
|
|
42
|
+
},
|
|
43
|
+
destroy() { source.destroy?.(); },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function streamingBody(reader, input, credit) {
|
|
48
|
+
const descriptor = input.bodyStream;
|
|
49
|
+
if (!descriptor || descriptor.version !== 1 || Object.keys(descriptor).some(key => !['version', 'length', 'maxBytes'].includes(key)) ||
|
|
50
|
+
descriptor.maxBytes !== MAX_BODY || !(descriptor.length === null || Number.isSafeInteger(descriptor.length) && descriptor.length >= 0 && descriptor.length <= MAX_BODY) ||
|
|
51
|
+
input.bodyLength !== undefined || input.body !== undefined || !Number.isSafeInteger(input.id) || input.id < 1 || ['GET', 'HEAD'].includes(input.method)) {
|
|
52
|
+
throw new Error('Invalid streaming request descriptor');
|
|
53
|
+
}
|
|
54
|
+
let controller, complete = false, total = 0, cancelled = false;
|
|
55
|
+
const state = { get complete() { return complete; }, get bytes() { return total; }, cancel() {
|
|
56
|
+
if (complete || cancelled) return;
|
|
57
|
+
cancelled = true;
|
|
58
|
+
try { controller?.error(new Error('Request body transport cancelled')); } catch {}
|
|
59
|
+
reader.destroy();
|
|
60
|
+
} };
|
|
61
|
+
const body = new ReadableStream({
|
|
62
|
+
start(value) { controller = value; },
|
|
63
|
+
async pull(value) {
|
|
64
|
+
try {
|
|
65
|
+
// A single explicit credit authorizes one frame. No speculative read
|
|
66
|
+
// consumes an upload while the application is not reading.
|
|
67
|
+
await credit({ id: input.id, type: 'bodycredit', length: REQUEST_BODY_CHUNK });
|
|
68
|
+
const header = await reader.exact(4), length = header.readUInt32BE();
|
|
69
|
+
if (length === 0xffffffff) {
|
|
70
|
+
const code = (await reader.exact(1))[0];
|
|
71
|
+
const kinds = { 1: ['PRNEXT_BODY_LIMIT', 413], 2: ['PRNEXT_BODY_TIMEOUT', 408], 3: ['PRNEXT_BODY_READ', 400], 4: ['PRNEXT_BODY_LENGTH', 400] };
|
|
72
|
+
if (!kinds[code]) throw new Error('Invalid request body error frame');
|
|
73
|
+
throw Object.assign(new Error('Request body transport failed'), { code: kinds[code][0], statusCode: kinds[code][1] });
|
|
74
|
+
}
|
|
75
|
+
if (length === 0) {
|
|
76
|
+
if (descriptor.length !== null && total !== descriptor.length) throw new Error('Request body length mismatch');
|
|
77
|
+
complete = true; value.close(); return;
|
|
78
|
+
}
|
|
79
|
+
if (length > REQUEST_BODY_CHUNK || total + length > MAX_BODY || descriptor.length !== null && total + length > descriptor.length) throw new Error('Request body exceeds its transport budget');
|
|
80
|
+
const bytes = await reader.exact(length); total += bytes.length; value.enqueue(bytes);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (cancelled) return;
|
|
83
|
+
value.error(error);
|
|
84
|
+
if (!/^PRNEXT_BODY_(LIMIT|TIMEOUT|READ|LENGTH)$/.test(error.code || '')) reader.destroy();
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
async cancel() { cancelled = true; await credit({ id: input.id, type: 'bodycancel' }); },
|
|
88
|
+
}, { highWaterMark: 0 });
|
|
89
|
+
Object.defineProperty(input, requestBodyState, { value: state });
|
|
90
|
+
return body;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function* requestFrames(source, { authenticate, streamBodies = false, credit } = {}) {
|
|
94
|
+
const reader = laneReader(source);
|
|
95
|
+
if (authenticate) {
|
|
96
|
+
const token = await reader.line(256);
|
|
97
|
+
if (token === undefined) throw new Error('Truncated request authentication');
|
|
98
|
+
authenticate(token);
|
|
99
|
+
}
|
|
100
|
+
for (;;) {
|
|
101
|
+
const line = await reader.line(MAX_HEADER);
|
|
102
|
+
if (line === undefined) return;
|
|
103
|
+
if (!line.trim()) continue;
|
|
104
|
+
const input = JSON.parse(line);
|
|
105
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('Invalid request frame');
|
|
106
|
+
if (input.bodyStream !== undefined) {
|
|
107
|
+
if (!authenticate || !streamBodies || typeof credit !== 'function') throw new Error('Streaming request protocol was not negotiated');
|
|
108
|
+
input.body = streamingBody(reader, input, credit);
|
|
109
|
+
try { yield input; }
|
|
110
|
+
finally { input[requestBodyState].cancel(); }
|
|
111
|
+
// Streaming upload lanes are single-use: an early response cannot turn
|
|
112
|
+
// unread upload bytes into another request. The Node host stays warm.
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (input.bodyLength !== undefined) {
|
|
35
116
|
if (!Number.isSafeInteger(input.bodyLength) || input.bodyLength < 0 || input.bodyLength > MAX_BODY) throw new Error('Invalid request body length');
|
|
36
|
-
|
|
37
|
-
else body = Buffer.allocUnsafe(input.bodyLength);
|
|
117
|
+
input.body = await reader.exact(input.bodyLength);
|
|
38
118
|
}
|
|
119
|
+
yield input;
|
|
39
120
|
}
|
|
40
|
-
if (size || body || authenticate) throw new Error('Truncated request transport frame');
|
|
41
121
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {lstat, realpath} from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const normalized = filename => process.platform === 'win32' ? path.resolve(filename).toLowerCase() : path.resolve(filename);
|
|
5
|
+
export const sameStoragePath = (left, right) => normalized(left) === normalized(right);
|
|
6
|
+
const ordinary = info => info?.isDirectory() && !info.isSymbolicLink();
|
|
7
|
+
const identity = (left, right) => left.dev === right.dev && left.ino === right.ino && left.birthtimeMs === right.birthtimeMs;
|
|
8
|
+
|
|
9
|
+
/** Callers supply canonical roots and explicit descendants, never glob results. */
|
|
10
|
+
export async function captureStorageDirectory(directory) {
|
|
11
|
+
try {
|
|
12
|
+
directory = path.resolve(directory);
|
|
13
|
+
const info = await lstat(directory);
|
|
14
|
+
if (!ordinary(info) || !sameStoragePath(await realpath(directory), directory)) return null;
|
|
15
|
+
const current = await lstat(directory);
|
|
16
|
+
if (!ordinary(current) || !identity(info, current)) return null;
|
|
17
|
+
return {directory, dev: info.dev, ino: info.ino, birthtimeMs: info.birthtimeMs};
|
|
18
|
+
} catch { return null; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Recheck ancestors around each filesystem operation. Node does not expose an
|
|
22
|
+
* atomic openat/unlinkat boundary: these guards reject observed replacements,
|
|
23
|
+
* but are not isolation from a malicious process with the same filesystem rights. */
|
|
24
|
+
export async function verifyStorageDirectories(snapshots) {
|
|
25
|
+
if (!Array.isArray(snapshots) || !snapshots.length) return false;
|
|
26
|
+
for (const snapshot of snapshots) {
|
|
27
|
+
if (!snapshot) return false;
|
|
28
|
+
try {
|
|
29
|
+
const info = await lstat(snapshot.directory);
|
|
30
|
+
if (!ordinary(info) || !identity(info, snapshot) || !sameStoragePath(await realpath(snapshot.directory), snapshot.directory)) return false;
|
|
31
|
+
const current = await lstat(snapshot.directory);
|
|
32
|
+
if (!ordinary(current) || !identity(current, snapshot)) return false;
|
|
33
|
+
} catch { return false; }
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
package/runtime/trace.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import {isControlError} from './navigation.mjs';
|
|
|
2
2
|
import {AsyncLocalStorage} from 'node:async_hooks';
|
|
3
3
|
import {randomBytes} from 'node:crypto';
|
|
4
4
|
import {createRequire} from 'node:module';
|
|
5
|
+
import {channel} from 'node:diagnostics_channel';
|
|
5
6
|
const scope=new AsyncLocalStorage(),rawFetch=globalThis.fetch;
|
|
6
7
|
let api;
|
|
7
8
|
try{api=createRequire(import.meta.url)('@opentelemetry/api');}catch{}
|
|
@@ -9,6 +10,17 @@ const endpoint=process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT||(process.env.OTEL
|
|
|
9
10
|
const protocol=process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL||process.env.OTEL_EXPORTER_OTLP_PROTOCOL;
|
|
10
11
|
const enabled=process.env.OTEL_SDK_DISABLED!=='true'&&endpoint&&(!protocol||protocol==='http/json');
|
|
11
12
|
const queue=[];let timer,flushing;
|
|
13
|
+
const diagnostics=channel('prnext.telemetry');
|
|
14
|
+
const counters={droppedSpans:0,rejectedSpans:0,exportAttempts:0,exportFailures:0,failedBatches:0,exportedSpans:0};
|
|
15
|
+
let lastWarning=-Infinity;
|
|
16
|
+
export function telemetryDiagnostics(){return {enabled:!!enabled,queuedSpans:queue.length,...counters};}
|
|
17
|
+
function notify(reason){
|
|
18
|
+
const snapshot=telemetryDiagnostics();
|
|
19
|
+
// This local channel and fixed-field warning never pass through the exporter.
|
|
20
|
+
// No collector URL, headers, exception or request attributes are included.
|
|
21
|
+
if(diagnostics.hasSubscribers)diagnostics.publish({reason,...snapshot});
|
|
22
|
+
if(reason!=='exported'&&Date.now()-lastWarning>=10000){lastWarning=Date.now();try{console.error('[prnext] telemetry '+JSON.stringify({reason,...snapshot}));}catch{}}
|
|
23
|
+
}
|
|
12
24
|
const epoch=BigInt(Date.now())*1000000n-process.hrtime.bigint();
|
|
13
25
|
const nanos=()=>String(epoch+process.hrtime.bigint());
|
|
14
26
|
export function parseTraceparent(value){
|
|
@@ -23,9 +35,25 @@ export function propagatedFetch(input,init){
|
|
|
23
35
|
return [input,{...init,headers}];
|
|
24
36
|
}
|
|
25
37
|
function enqueue(span){
|
|
26
|
-
if(!enabled||!span.flags
|
|
38
|
+
if(!enabled||!span.flags)return;
|
|
39
|
+
if(queue.length>=1024){counters.droppedSpans++;notify('queue_full');return;}
|
|
27
40
|
queue.push(span);if(!timer)timer=setTimeout(()=>{timer=undefined;void flushTraces();},200).unref();
|
|
28
41
|
}
|
|
42
|
+
async function collectorResult(response,count){
|
|
43
|
+
const reader=response.body?.getReader();if(!reader)return {rejected:0,warning:false};
|
|
44
|
+
const chunks=[];let size=0;
|
|
45
|
+
try{while(true){const {done,value}=await reader.read();if(done)break;size+=value.byteLength;if(size>16384)throw Error('collector response too large');chunks.push(value);}}
|
|
46
|
+
catch(error){await reader.cancel().catch(()=>{});throw error;}finally{reader.releaseLock();}
|
|
47
|
+
const body=size?JSON.parse(Buffer.concat(chunks,size).toString('utf8')):{};
|
|
48
|
+
if(!body||typeof body!=='object'||Array.isArray(body))throw Error('invalid collector response');
|
|
49
|
+
const partial=body.partialSuccess??body.partial_success;if(partial==null)return {rejected:0,warning:false};
|
|
50
|
+
if(typeof partial!=='object'||Array.isArray(partial))throw Error('invalid collector partial success');
|
|
51
|
+
const raw=partial.rejectedSpans??partial.rejected_spans??0;
|
|
52
|
+
if(typeof raw!=='number'&&!(typeof raw==='string'&&/^\d{1,20}$/.test(raw)))throw Error('invalid collector rejection count');
|
|
53
|
+
const rejected=Number(raw);
|
|
54
|
+
if(!Number.isSafeInteger(rejected)||rejected<0||rejected>count)throw Error('invalid collector rejection count');
|
|
55
|
+
return {rejected,warning:typeof(partial.errorMessage??partial.error_message)==='string'&&!!(partial.errorMessage??partial.error_message)};
|
|
56
|
+
}
|
|
29
57
|
export async function flushTraces(){
|
|
30
58
|
if(flushing)return flushing;
|
|
31
59
|
if(!queue.length)return;
|
|
@@ -36,15 +64,27 @@ export async function flushTraces(){
|
|
|
36
64
|
const spans=queue.splice(0,128);
|
|
37
65
|
flushing=(async()=>{
|
|
38
66
|
const body=JSON.stringify({resourceSpans:[{resource:{attributes:[{key:'service.name',value:{stringValue:process.env.OTEL_SERVICE_NAME||'prnext'}}]},scopeSpans:[{scope:{name:'prnext.runtime'},spans}]}]});
|
|
67
|
+
let exported=false;
|
|
39
68
|
for(let attempt=0;attempt<3;attempt++){
|
|
40
|
-
let retry=false;
|
|
69
|
+
let retry=false,receivedSuccess=false,accepted;
|
|
70
|
+
counters.exportAttempts++;
|
|
41
71
|
try{
|
|
42
72
|
const response=await rawFetch(endpoint,{method:'POST',redirect:'error',headers,body,signal:AbortSignal.timeout(1000)});
|
|
43
|
-
|
|
44
|
-
|
|
73
|
+
receivedSuccess=response.ok;
|
|
74
|
+
if(receivedSuccess)accepted=await collectorResult(response,spans.length);
|
|
75
|
+
else{retry=[429,502,503,504].includes(response.status);await response.body?.cancel();}
|
|
76
|
+
}catch{retry=!receivedSuccess;} // Never replay a successful/partial acknowledgement.
|
|
77
|
+
if(accepted){
|
|
78
|
+
exported=true;counters.exportedSpans+=spans.length-accepted.rejected;
|
|
79
|
+
if(accepted.rejected){counters.rejectedSpans+=accepted.rejected;counters.droppedSpans+=accepted.rejected;counters.exportFailures++;counters.failedBatches++;notify('collector_rejected');}
|
|
80
|
+
else notify(accepted.warning?'collector_warning':'exported');
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
counters.exportFailures++;
|
|
45
84
|
if(!retry)break;
|
|
46
85
|
if(attempt<2)await new Promise(resolve=>setTimeout(resolve,100*2**attempt).unref());
|
|
47
86
|
}
|
|
87
|
+
if(!exported){counters.failedBatches++;counters.droppedSpans+=spans.length;notify('export_failed');}
|
|
48
88
|
})().finally(()=>{flushing=undefined;if(queue.length&&!timer)timer=setTimeout(()=>{timer=undefined;void flushTraces();},200).unref();});
|
|
49
89
|
return flushing;
|
|
50
90
|
}
|
package/runtime/worker.mjs
CHANGED
|
@@ -142,6 +142,9 @@ async function handleRequest(input, output, signalController, responseComplete,
|
|
|
142
142
|
? { ...manifest.middleware, id: '__prnext_middleware', kind: 'middleware' }
|
|
143
143
|
: routes.get(input.routeId);
|
|
144
144
|
if (!route) { await output(input.id, { status: 404, body: 'Not Found' }, input); return; }
|
|
145
|
+
if (input.bodyStream && (route.router !== 'app' || route.kind !== 'api' || input.renderMode || !manifest.config?.streamingRequestBodies)) {
|
|
146
|
+
throw new Error('Streaming request bodies require an enabled App Route Handler');
|
|
147
|
+
}
|
|
145
148
|
const sourceModules=!production&&route.router==='app'&&route.kind==='page'?advancedRoutingSources({...input,basePath:manifest.config?.basePath||''},manifest.routes):[];
|
|
146
149
|
const deferredSources=sourceModules.length?manifest.routes.filter(candidate=>candidate.devDeferred&&sourceModules.includes(candidate.module)).map(candidate=>candidate.pattern):[];
|
|
147
150
|
if(route.devDeferred||deferredSources.length){
|
|
@@ -197,7 +200,9 @@ async function handleRequest(input, output, signalController, responseComplete,
|
|
|
197
200
|
// The application's scope was aborted above, but the lane is still valid
|
|
198
201
|
// for its one error/redirect response. Do not mistake that cleanup signal
|
|
199
202
|
// for a client disconnect while writing the replacement headers.
|
|
200
|
-
|
|
203
|
+
const ingressFailure = /^PRNEXT_BODY_(LIMIT|TIMEOUT|READ|LENGTH)$/.test(error?.code || '')
|
|
204
|
+
? { status: error.statusCode, headers: { 'content-type': 'text/plain; charset=utf-8' }, body: 'Request body transport failed' } : undefined;
|
|
205
|
+
await output(input?.id ?? null, ingressFailure || navigationResponse(error) || errorResponse(error, production), { ...input, production });
|
|
201
206
|
responseComplete?.();
|
|
202
207
|
} finally {
|
|
203
208
|
if (finalizers.size) {
|
|
@@ -261,7 +266,7 @@ if (token && laneLimit > 1) {
|
|
|
261
266
|
const candidate = Buffer.from(line);
|
|
262
267
|
if (candidate.length !== expected.length || !timingSafeEqual(candidate, expected)) throw new Error('Worker authentication failed');
|
|
263
268
|
clearTimeout(authTimer); socket.write('ready\n');
|
|
264
|
-
} })) {
|
|
269
|
+
}, streamBodies: true, credit: frame => new Promise((resolve, reject) => socket.write(JSON.stringify(frame) + '\n', error => error ? reject(error) : resolve())) })) {
|
|
265
270
|
if (invocations.size >= laneLimit) throw new Error('Worker invocation capacity exceeded');
|
|
266
271
|
controller = new AbortController();
|
|
267
272
|
const active = controller;
|
|
@@ -339,10 +344,10 @@ if (token && laneLimit > 1) {
|
|
|
339
344
|
});
|
|
340
345
|
server.listen(0, '127.0.0.1', () => {
|
|
341
346
|
if(!production&&typeof globalThis.__PRNEXT_DEV_WORKER_READY__==='function'){
|
|
342
|
-
globalThis.__PRNEXT_DEV_WORKER_READY__({port:server.address().port,binary:true,gracefulShutdown:true});
|
|
347
|
+
globalThis.__PRNEXT_DEV_WORKER_READY__({port:server.address().port,binary:true,gracefulShutdown:true,requestBodyStream:1});
|
|
343
348
|
delete globalThis.__PRNEXT_DEV_WORKER_READY__;
|
|
344
349
|
}
|
|
345
|
-
writeProtocol(JSON.stringify({ port: server.address().port, binary: true, heartbeat: true, activity: reportActivity, gracefulShutdown: true }) + '\n');
|
|
350
|
+
writeProtocol(JSON.stringify({ port: server.address().port, binary: true, heartbeat: true, activity: reportActivity, gracefulShutdown: true, requestBodyStream: 1 }) + '\n');
|
|
346
351
|
heartbeat = setInterval(() => {
|
|
347
352
|
// Coalesce the idle transition with the existing heartbeat. Normal
|
|
348
353
|
// request completion adds no stdout messages, and short cache bursts
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Fontkit 2.0.4 Node create() bundle, generated by scripts/prepare-fontkit.mjs.
|
|
2
|
+
Upstream: https://github.com/foliojs/fontkit
|
|
3
|
+
The original code is bundled/minified without patches. Source copyright notices, dependency licenses, README license declarations and exact versions/integrities are retained alongside it.
|
|
4
|
+
fontkit, dfa and brotli declare MIT in their package metadata/README and publish no separate license file. brotli also contains Google Apache-2.0 source notices, retained in SOURCE-NOTICES.txt; the Apache-2.0 text is included with @swc/helpers.
|
|
5
|
+
Use upstream.json to audit/update this bundled dependency when security fixes are released.
|