@thomas.f/prnext 0.1.13-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 +53 -42
- package/build/ADAPTERS.md +121 -0
- package/build/app.mjs +3 -0
- package/build/build-lock.mjs +71 -0
- package/build/index.mjs +7 -1
- package/build/inspect.mjs +20 -2
- package/build/inspection-summary.mjs +37 -0
- package/build/og.mjs +3 -9
- package/build/output-contract.mjs +41 -0
- package/build/precompress.mjs +13 -0
- package/build/scan.mjs +1 -1
- package/build/static-adapter.mjs +100 -0
- package/cli.mjs +48 -11
- package/compat/diagnostics.cjs +4 -0
- package/compat/index.d.ts +29 -0
- package/index.mjs +2 -0
- package/package.json +8 -9
- package/release/verify.mjs +2 -0
- package/runtime/api.mjs +5 -2
- package/runtime/cloud/archive.mjs +2 -1
- package/runtime/cloud/client.mjs +2 -19
- package/runtime/cloud/state.mjs +16 -1
- 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
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { copyFile, lstat, mkdir, readdir, realpath, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { rename } from '../runtime/fs.mjs';
|
|
6
|
+
import { OUTPUT_POINTER, prepareBuildDirectory, readBuildDirectory, validateDistDir } from '../runtime/build-directory.mjs';
|
|
7
|
+
import { writeBuildOutput } from './output-contract.mjs';
|
|
8
|
+
import { precompressBuild } from './precompress.mjs';
|
|
9
|
+
import { withBuildLock } from './build-lock.mjs';
|
|
10
|
+
|
|
11
|
+
const MAX_FILES = 20000, MAX_BYTES = 512 * 1024 * 1024;
|
|
12
|
+
const inside = (root, file) => file.startsWith(root + path.sep);
|
|
13
|
+
const urlFor = file => '/' + file.split('/').map(encodeURIComponent).join('/');
|
|
14
|
+
|
|
15
|
+
/** Import an already-built public directory. Never evaluate its config or code. */
|
|
16
|
+
export async function buildStatic(directory, { from = 'dist', spa = false, distDir, dev = false } = {}) {
|
|
17
|
+
return withBuildLock(directory, root => importStatic(root, { from, spa, distDir, dev }));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function importStatic(directory, { from, spa, distDir, dev }) {
|
|
21
|
+
if (dev) throw new Error('The static adapter imports production files; use the source framework for development.');
|
|
22
|
+
if (typeof spa !== 'boolean') throw new TypeError('spa must be a boolean');
|
|
23
|
+
const root = await realpath(directory);
|
|
24
|
+
// Reuse the output-directory restrictions to keep imports within this project
|
|
25
|
+
// and away from source/dependency/repository trees.
|
|
26
|
+
const relative = validateDistDir(from);
|
|
27
|
+
const output = distDir === undefined ? await readBuildDirectory(root) : validateDistDir(distDir);
|
|
28
|
+
const source = path.join(root, relative), destination = await prepareBuildDirectory(root, output);
|
|
29
|
+
if (source === destination || inside(source, destination) || inside(destination, source)) throw new Error('Static input and PRNext output directories must not overlap.');
|
|
30
|
+
let ancestor = root;
|
|
31
|
+
for (const part of relative.split('/')) {
|
|
32
|
+
ancestor = path.join(ancestor, part);
|
|
33
|
+
const entry = await lstat(ancestor);
|
|
34
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) throw new Error('Static input must use real directories, not symbolic links.');
|
|
35
|
+
}
|
|
36
|
+
if (!inside(root, await realpath(source))) throw new Error('Static input must stay inside the project.');
|
|
37
|
+
const index = await lstat(path.join(source, 'index.html')).catch(() => null);
|
|
38
|
+
if (!index?.isFile() || index.isSymbolicLink()) throw new Error('Static input needs index.html. Run your framework build first and select its public output with --from.');
|
|
39
|
+
|
|
40
|
+
const id = randomUUID(), stage = path.join(root, `.prnext-build-${id}`);
|
|
41
|
+
const backup = path.join(root, `.prnext-backup-${id}`), pointer = path.join(root, `.prnext-output-${id}.tmp`);
|
|
42
|
+
const manifest = { version: 1, buildId: id, config: { staticOnly: true, staticFallback: spa, compress: true, poweredByHeader: true }, routes: [], prerendered: [] };
|
|
43
|
+
const paths = new Set();
|
|
44
|
+
let count = 0, bytes = 0, previous = false, published = false;
|
|
45
|
+
const add = (url, file) => {
|
|
46
|
+
if (paths.has(url)) throw new Error(`Ambiguous static URL ${url}; remove the conflicting file or HTML alias.`);
|
|
47
|
+
paths.add(url);
|
|
48
|
+
manifest.prerendered.push({ path: url, file: `static/${file}`, revalidate: false });
|
|
49
|
+
};
|
|
50
|
+
async function copy(folder = '', depth = 0) {
|
|
51
|
+
if (depth > 64) throw new Error('Static input exceeds 64 directory levels.');
|
|
52
|
+
for (const name of (await readdir(path.join(source, folder))).sort()) {
|
|
53
|
+
if (name.includes('\\') || /[\x00-\x1f\x7f:]/.test(name) ||
|
|
54
|
+
name.startsWith('.') && !(folder === '' && name === '.well-known') ||
|
|
55
|
+
['node_modules', '_prnext', '_next'].includes(name) || folder === '' && name === 'server') {
|
|
56
|
+
throw new Error(`Static input contains a private, reserved or server path: ${folder ? folder + '/' : ''}${name}. Select only browser-ready files.`);
|
|
57
|
+
}
|
|
58
|
+
const file = folder ? `${folder}/${name}` : name;
|
|
59
|
+
const entry = await lstat(path.join(source, file));
|
|
60
|
+
if (entry.isSymbolicLink()) throw new Error(`Static input cannot contain symbolic links: ${file}`);
|
|
61
|
+
if (entry.isDirectory()) { await copy(file, depth + 1); continue; }
|
|
62
|
+
if (!entry.isFile()) throw new Error(`Static input must contain regular files: ${file}`);
|
|
63
|
+
if (++count > MAX_FILES || (bytes += entry.size) > MAX_BYTES) throw new Error('Static input exceeds 20,000 files or 512 MiB.');
|
|
64
|
+
const target = path.join(stage, 'static', file);
|
|
65
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
66
|
+
await copyFile(path.join(source, file), target, constants.COPYFILE_EXCL);
|
|
67
|
+
const after = await lstat(path.join(source, file));
|
|
68
|
+
if (!after.isFile() || after.size !== entry.size || after.mtimeMs !== entry.mtimeMs || (await stat(target)).size !== entry.size) throw new Error(`Static input changed while being imported: ${file}. Retry after its build finishes.`);
|
|
69
|
+
add(urlFor(file), file);
|
|
70
|
+
if (file === 'index.html') add('/', file);
|
|
71
|
+
else if (file.endsWith('/index.html')) add(urlFor(file.slice(0, -11)), file);
|
|
72
|
+
else if (file.endsWith('.html')) add(urlFor(file.slice(0, -5)), file);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
await mkdir(stage);
|
|
77
|
+
await copy();
|
|
78
|
+
await writeFile(path.join(stage, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
79
|
+
await writeBuildOutput(stage, manifest, { adapter: 'static', spa });
|
|
80
|
+
await precompressBuild(stage, manifest, { cacheDirectory: path.join(root, '.prnext-cache/precompress-v1') });
|
|
81
|
+
// A failed import leaves the previously published build and pointer intact.
|
|
82
|
+
previous = await lstat(destination).then(() => true, error => { if (error.code === 'ENOENT') return false; throw error; });
|
|
83
|
+
await writeFile(pointer, JSON.stringify({ distDir: output }) + '\n', { flag: 'wx' });
|
|
84
|
+
if (previous) await rename(destination, backup);
|
|
85
|
+
try {
|
|
86
|
+
await rename(stage, destination);
|
|
87
|
+
published = true;
|
|
88
|
+
await rename(pointer, path.join(root, OUTPUT_POINTER));
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (published) await rename(destination, stage);
|
|
91
|
+
if (previous) await rename(backup, destination);
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
if (previous) await rm(backup, { recursive: true, force: true });
|
|
95
|
+
return { ...manifest, outputDirectory: destination, files: count, bytes };
|
|
96
|
+
} finally {
|
|
97
|
+
await rm(pointer, { force: true });
|
|
98
|
+
await rm(stage, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
}
|
package/cli.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { rename } from './runtime/fs.mjs';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
|
-
import { readFile, writeFile, rm } from 'node:fs/promises';
|
|
4
|
+
import { readFile, writeFile, rm, mkdir, readdir } from 'node:fs/promises';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { resolveNativeBinary, packageManifest } from './native/resolve.mjs';
|
|
@@ -16,6 +16,9 @@ const help = `PRNext ${packageManifest.version}
|
|
|
16
16
|
Use prnext for portable scripts; prn is a shortcut on compatible shells.
|
|
17
17
|
|
|
18
18
|
prnext build [directory] Build React/TypeScript and static HTML
|
|
19
|
+
prnext build [directory] --adapter static [--from dist] [--spa]
|
|
20
|
+
Import an existing public build directory
|
|
21
|
+
prnext output [directory] [--json] Read the versioned build output contract
|
|
19
22
|
prnext start [directory] [options] Start native Rust production server
|
|
20
23
|
prnext pstart [directory] [options] Start production persistently in the background
|
|
21
24
|
prnext prestart [name] Replace a persistent app after a health check
|
|
@@ -27,7 +30,7 @@ Use prnext for portable scripts; prn is a shortcut on compatible shells.
|
|
|
27
30
|
prnext dev [directory] [options] Rebuild and restart on source changes
|
|
28
31
|
prnext routes [directory] Display compiled routes
|
|
29
32
|
prnext check [directory] [--json] Check an existing Next project before building
|
|
30
|
-
prnext inspect [directory] [--json]
|
|
33
|
+
prnext inspect [directory] [--json|--otlp] Explain routes, health and sampled timings
|
|
31
34
|
prnext host [prnext.host.json] Host built apps by hostname; wake on demand
|
|
32
35
|
prnext host [config] --check Validate hosting configuration and builds
|
|
33
36
|
prnext host [config] --status [--json] Show memory, process and idle status
|
|
@@ -52,7 +55,7 @@ Memory favors lower RAM under concurrency, accepting more waiting and CPU per re
|
|
|
52
55
|
Without an option: PRNEXT_PROFILE, legacy PRNEXT_MEMORY_PROFILE=compact, then balanced.
|
|
53
56
|
Classic preserves the former standard settings; standard remains an alias. Legacy compact remains accepted.
|
|
54
57
|
Development includes React Fast Refresh, stylesheet updates and an error overlay.
|
|
55
|
-
Requires Node
|
|
58
|
+
Requires Node 22.23.4+, 24.22.1+ or 26.12.1+. Published packages include the native server for supported platforms.
|
|
56
59
|
Rust is only needed when building the framework from source.
|
|
57
60
|
`;
|
|
58
61
|
|
|
@@ -147,19 +150,36 @@ async function main() {
|
|
|
147
150
|
if (!report.ok) process.exitCode = 1;
|
|
148
151
|
return;
|
|
149
152
|
}
|
|
150
|
-
if (!['build', 'start', 'dev', 'routes', 'check', 'inspect'].includes(command)) throw new Error(`Unknown command: ${command}\n${help}`);
|
|
153
|
+
if (!['build', 'start', 'dev', 'routes', 'check', 'inspect', 'output'].includes(command)) throw new Error(`Unknown command: ${command}\n${help}`);
|
|
151
154
|
const args = [...argv];
|
|
152
155
|
const root = path.resolve(args[0] && !args[0].startsWith('-') ? args.shift() : '.');
|
|
156
|
+
if (command === 'output') {
|
|
157
|
+
if (args.some(arg => arg !== '--json')) throw new Error('output accepts only [directory] and --json');
|
|
158
|
+
const { readBuildOutput } = await import('./build/output-contract.mjs');
|
|
159
|
+
const output = await readBuildOutput(path.join(root, await readBuildDirectory(root)));
|
|
160
|
+
if (args.includes('--json')) console.log(JSON.stringify(output, null, 2));
|
|
161
|
+
else console.log(`PRNext output v${output.version}: ${output.adapter}\nBuild: ${output.buildId}\nManifest: ${output.manifest}\nAssets: ${output.assets}\nServer: ${output.server?.entry || 'none'}\nSPA fallback: ${output.fallback || 'disabled'}`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
153
164
|
if (command === 'inspect') {
|
|
154
|
-
if (args.some(arg => arg
|
|
155
|
-
const { inspectProject, printInspection } = await import('./build/inspect.mjs');
|
|
165
|
+
if (args.some(arg => !['--json', '--otlp'].includes(arg)) || args.includes('--json') && args.includes('--otlp')) throw new Error('inspect accepts [directory] and either --json or --otlp');
|
|
166
|
+
const { inspectProject, printInspection, inspectionMetrics } = await import('./build/inspect.mjs');
|
|
156
167
|
const report = await inspectProject(root);
|
|
157
|
-
if (args.includes('--
|
|
168
|
+
if (args.includes('--otlp')) console.log(JSON.stringify(inspectionMetrics(report), null, 2));
|
|
169
|
+
else if (args.includes('--json')) console.log(JSON.stringify(report, null, 2)); else printInspection(report);
|
|
158
170
|
return;
|
|
159
171
|
}
|
|
160
172
|
if (['start', 'dev'].includes(command) && args.includes('--inspect')) {
|
|
161
173
|
args.splice(args.indexOf('--inspect'), 1);
|
|
162
174
|
process.env.PRNEXT_INSPECT_DIR = path.join(root, '.prnext-cache/inspect');
|
|
175
|
+
await mkdir(process.env.PRNEXT_INSPECT_DIR, {recursive:true, mode:0o700});
|
|
176
|
+
process.env.PRNEXT_METRICS_FILE ||= path.join(process.env.PRNEXT_INSPECT_DIR, `metrics-${process.pid}.jsonl`);
|
|
177
|
+
const old = (await readdir(process.env.PRNEXT_INSPECT_DIR)).filter(name => /^metrics-\d+\.jsonl$/.test(name));
|
|
178
|
+
for (const name of old.slice(0, Math.max(0, old.length - 4))) {
|
|
179
|
+
const pid = Number(name.slice(8,-6));
|
|
180
|
+
try { process.kill(pid, 0); }
|
|
181
|
+
catch (error) { if (error.code === 'ESRCH') await rm(path.join(process.env.PRNEXT_INSPECT_DIR,name),{force:true}); }
|
|
182
|
+
}
|
|
163
183
|
}
|
|
164
184
|
if (command === 'build' || command === 'dev') {
|
|
165
185
|
// Build plugins such as Contentlayer read cwd/INIT_CWD rather than webpack's
|
|
@@ -183,13 +203,30 @@ async function main() {
|
|
|
183
203
|
return;
|
|
184
204
|
}
|
|
185
205
|
if (command === 'build') {
|
|
186
|
-
const
|
|
187
|
-
|
|
206
|
+
const options = {};
|
|
207
|
+
for (let i = 0; i < args.length; i++) {
|
|
208
|
+
const flag = args[i];
|
|
209
|
+
if (flag === '--dev') options.dev = true;
|
|
210
|
+
else if (flag === '--spa') options.spa = true;
|
|
211
|
+
else if (flag === '--adapter' || flag === '--from') {
|
|
212
|
+
const value = args[++i];
|
|
213
|
+
if (!value || value.startsWith('-')) throw new Error(`${flag} requires a value`);
|
|
214
|
+
const key = flag.slice(2);
|
|
215
|
+
if (options[key] !== undefined) throw new Error(`Duplicate build option: ${flag}`);
|
|
216
|
+
options[key] = value;
|
|
217
|
+
} else throw new Error(`Unknown build option: ${flag}`);
|
|
218
|
+
}
|
|
219
|
+
const dev = !!options.dev;
|
|
188
220
|
const envMode = process.env.NODE_ENV === 'test' ? 'test' : undefined;
|
|
189
221
|
process.env.NODE_ENV = dev ? 'development' : 'production';
|
|
190
|
-
const { build } = await import('./build/index.mjs');
|
|
191
222
|
const started = performance.now();
|
|
192
|
-
|
|
223
|
+
if (options.adapter === 'static') {
|
|
224
|
+
const { buildStatic } = await import('./build/static-adapter.mjs');
|
|
225
|
+
await buildStatic(root, options);
|
|
226
|
+
} else {
|
|
227
|
+
const { build } = await import('./build/index.mjs');
|
|
228
|
+
await build(root, { ...options, dev, envMode });
|
|
229
|
+
}
|
|
193
230
|
console.log(`PRNext built ${root} in ${((performance.now() - started) / 1000).toFixed(2)}s`);
|
|
194
231
|
return;
|
|
195
232
|
}
|
package/compat/diagnostics.cjs
CHANGED
|
@@ -4,6 +4,10 @@ const directory = process.env.PRNEXT_INSPECT_DIR;
|
|
|
4
4
|
const { threadId } = require('node:worker_threads');
|
|
5
5
|
let queue = [], scheduled = false, writing = false, size;
|
|
6
6
|
const limit = 1024 * 1024;
|
|
7
|
+
if (directory) setInterval(() => {
|
|
8
|
+
const {rss, heapUsed} = process.memoryUsage();
|
|
9
|
+
record('process', {rssBytes:rss, heapUsedBytes:heapUsed});
|
|
10
|
+
}, 5000).unref();
|
|
7
11
|
function record(type, fields = {}) {
|
|
8
12
|
if (!directory || queue.length >= 256) return;
|
|
9
13
|
const line = JSON.stringify({ time: Date.now(), pid: process.pid, type, ...fields }) + '\n';
|
package/compat/index.d.ts
CHANGED
|
@@ -168,4 +168,33 @@ export interface NextConfig {
|
|
|
168
168
|
rewrites?: () => Rewrite[] | { beforeFiles?: Rewrite[]; afterFiles?: Rewrite[]; fallback?: Rewrite[] } | Promise<Rewrite[] | { beforeFiles?: Rewrite[]; afterFiles?: Rewrite[]; fallback?: Rewrite[] }>;
|
|
169
169
|
}
|
|
170
170
|
export type PRNextConfig = NextConfig;
|
|
171
|
+
/** Public build envelope; internal manifest fields are adapter-specific. */
|
|
172
|
+
export type BuildOutput = {
|
|
173
|
+
version: 1;
|
|
174
|
+
buildId: string;
|
|
175
|
+
manifest: 'manifest.json';
|
|
176
|
+
} & ({
|
|
177
|
+
adapter: 'react';
|
|
178
|
+
assets: 'assets';
|
|
179
|
+
server: { entry: 'runtime/worker.mjs'; runtimeContract: 'runtime-contract.json' };
|
|
180
|
+
fallback: null;
|
|
181
|
+
} | {
|
|
182
|
+
adapter: 'static';
|
|
183
|
+
assets: 'static';
|
|
184
|
+
server: null;
|
|
185
|
+
fallback: null | 'index.html';
|
|
186
|
+
});
|
|
187
|
+
export interface StaticBuildOptions {
|
|
188
|
+
from?: string;
|
|
189
|
+
spa?: boolean;
|
|
190
|
+
distDir?: string;
|
|
191
|
+
dev?: false;
|
|
192
|
+
}
|
|
193
|
+
export interface BuildResult { buildId: string; outputDirectory: string; }
|
|
194
|
+
export interface StaticBuildResult extends BuildResult { files: number; bytes: number; }
|
|
195
|
+
export function build(projectRoot: string, options: StaticBuildOptions & { adapter: 'static' }): Promise<StaticBuildResult>;
|
|
196
|
+
export function build(projectRoot: string, options?: { adapter?: 'react'; dev?: boolean; envMode?: 'production' | 'development' | 'test' }): Promise<BuildResult>;
|
|
197
|
+
export function buildStatic(projectRoot: string, options?: StaticBuildOptions): Promise<StaticBuildResult>;
|
|
198
|
+
export function readBuildOutput(buildDirectory: string): Promise<BuildOutput>;
|
|
199
|
+
export function validateBuildOutput(value: unknown): BuildOutput;
|
|
171
200
|
export type { DocumentContext, DocumentInitialProps, DocumentProps } from './document.cjs';
|
package/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thomas.f/prnext",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15-alpha",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Experimental Rust-powered runtime for Next.js apps, React and npm",
|
|
6
6
|
"homepage": "https://prnext.dev",
|
|
@@ -111,7 +111,6 @@
|
|
|
111
111
|
"@babel/types": "^7.29.0",
|
|
112
112
|
"@edge-runtime/vm": "5.0.0",
|
|
113
113
|
"@vercel/nft": "1.11.0",
|
|
114
|
-
"@vercel/og": "1.0.3",
|
|
115
114
|
"dotenv": "16.6.1",
|
|
116
115
|
"dotenv-expand": "11.0.7",
|
|
117
116
|
"esbuild": "^0.28.2",
|
|
@@ -133,14 +132,14 @@
|
|
|
133
132
|
"react-server-dom-webpack": "19.3.0"
|
|
134
133
|
},
|
|
135
134
|
"optionalDependencies": {
|
|
136
|
-
"prnext-darwin-arm64": "0.1.
|
|
137
|
-
"prnext-darwin-x64": "0.1.
|
|
138
|
-
"prnext-linux-arm64-gnu": "0.1.
|
|
139
|
-
"prnext-linux-x64-gnu": "0.1.
|
|
140
|
-
"@thomas.f/prnext-win32-x64-msvc": "0.1.
|
|
141
|
-
"@thomas.f/prnext-win32-arm64-msvc": "0.1.
|
|
135
|
+
"prnext-darwin-arm64": "0.1.15-alpha",
|
|
136
|
+
"prnext-darwin-x64": "0.1.15-alpha",
|
|
137
|
+
"prnext-linux-arm64-gnu": "0.1.15-alpha",
|
|
138
|
+
"prnext-linux-x64-gnu": "0.1.15-alpha",
|
|
139
|
+
"@thomas.f/prnext-win32-x64-msvc": "0.1.15-alpha",
|
|
140
|
+
"@thomas.f/prnext-win32-arm64-msvc": "0.1.15-alpha"
|
|
142
141
|
},
|
|
143
142
|
"engines": {
|
|
144
|
-
"node": ">=
|
|
143
|
+
"node": "^22.23.4 || ^24.22.1 || >=26.12.1"
|
|
145
144
|
}
|
|
146
145
|
}
|
package/release/verify.mjs
CHANGED
|
@@ -22,6 +22,8 @@ export async function verifyPackage() {
|
|
|
22
22
|
}
|
|
23
23
|
await exported(pkg.exports);
|
|
24
24
|
await access(path.join(packageRoot, 'runtime/profiles.json'));
|
|
25
|
+
for (const name of ['index.node.mjs', 'resvg.wasm', 'hb.wasm', 'Geist-Regular.ttf', 'LICENSE', 'LICENSE.fflate', 'LICENSE.Geist', 'LICENSE.harfbuzzjs', 'LICENSE.satori', 'NOTICE.txt']) await access(path.join(packageRoot, 'runtime/og', name));
|
|
26
|
+
if (pkg.dependencies['@vercel/og'] || (await readFile(path.join(packageRoot, 'runtime/og/index.node.mjs'), 'utf8')).includes('fflate@0.7.3')) throw new Error('OG renderer still depends on vulnerable upstream fflate. Run scripts/prepare-og.mjs.');
|
|
25
27
|
const source = sourceCheckout();
|
|
26
28
|
if (source) {
|
|
27
29
|
const cargo = await readFile(path.join(source, 'crates/prnext/Cargo.toml'), 'utf8');
|
package/runtime/api.mjs
CHANGED
|
@@ -131,7 +131,7 @@ async function runStreamingPagesApi(handler, request, { method, signal, timeoutM
|
|
|
131
131
|
const timer = setTimeout(() => cancelRequest(apiTimeoutError('response headers', timeoutMs)), timeoutMs);
|
|
132
132
|
timer.unref();
|
|
133
133
|
if (controller.signal.aborted) cancelRequest(abortError(controller.signal.reason));
|
|
134
|
-
else trackRequestWork(Promise.resolve().then(() => handler(request, response)), signal).then(result => {
|
|
134
|
+
else trackRequestWork(Promise.resolve().then(() => handler(request, response)), signal, { timeoutMs }).then(result => {
|
|
135
135
|
if (result !== undefined && result !== response && !response.writableEnded) {
|
|
136
136
|
throw new Error('Pages API handlers must send their response with res.send(), res.json(), or res.end()');
|
|
137
137
|
}
|
|
@@ -255,7 +255,10 @@ async function runApiInner({ modulePath, route = {}, manifest = {}, production =
|
|
|
255
255
|
if (lifecycle) pendingResponse.then(({ result }) => {
|
|
256
256
|
if (lifecycle.controller.signal.aborted) result?.body?.cancel(lifecycle.controller.signal.reason).catch(() => {});
|
|
257
257
|
}, () => {});
|
|
258
|
-
|
|
258
|
+
// A client disconnect does not cancel arbitrary npm/database promises.
|
|
259
|
+
// Let them settle within this route's existing deadline before retiring the
|
|
260
|
+
// shared worker; the shorter orphan guard would interrupt other visitors.
|
|
261
|
+
trackRequestWork(pendingResponse, signal, { timeoutMs });
|
|
259
262
|
let result, changedCookies, runInContext;
|
|
260
263
|
try {
|
|
261
264
|
({ result, changedCookies, runInContext } = await withSignal(pendingResponse, lifecycle?.controller.signal));
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { deflateRawSync } from 'node:zlib';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import picomatch from 'picomatch';
|
|
6
|
+
import { prospectivePath } from './state.mjs';
|
|
6
7
|
|
|
7
8
|
const MiB = 1048576;
|
|
8
9
|
const alwaysExcluded = (name) =>
|
|
@@ -96,7 +97,7 @@ export async function archiveProject(
|
|
|
96
97
|
) {
|
|
97
98
|
const root = await realpath(directory);
|
|
98
99
|
if (outputFile) {
|
|
99
|
-
const relative = path.relative(root,
|
|
100
|
+
const relative = path.relative(root,await prospectivePath(outputFile));
|
|
100
101
|
if (!relative || relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)) throw new Error('Temporary archive must be outside the project.');
|
|
101
102
|
}
|
|
102
103
|
const output = outputFile ? await open(outputFile, 'wx', 0o600) : null;
|
package/runtime/cloud/client.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { detectEnvFiles, readDeploymentEnv, confirmEnvImport } from './environme
|
|
|
10
10
|
import {
|
|
11
11
|
cloudOrigin,
|
|
12
12
|
credentialPath,
|
|
13
|
+
prospectivePath,
|
|
13
14
|
linkPath,
|
|
14
15
|
readState,
|
|
15
16
|
writeState,
|
|
@@ -35,24 +36,6 @@ Source excludes secrets/build output and respects root .gitignore/.prnextignore.
|
|
|
35
36
|
`;
|
|
36
37
|
const clean = (value) =>
|
|
37
38
|
String(value || '').replace(/[\x00-\x08\x0b-\x1f\x7f]/g, '');
|
|
38
|
-
|
|
39
|
-
async function resolveCredentialDirectory(directory) {
|
|
40
|
-
let existing = directory;
|
|
41
|
-
const missing = [];
|
|
42
|
-
for (;;) {
|
|
43
|
-
try {
|
|
44
|
-
return path.join(await realpath(existing), ...missing);
|
|
45
|
-
} catch (error) {
|
|
46
|
-
const parent = path.dirname(existing);
|
|
47
|
-
if (error.code !== 'ENOENT' || parent === existing) throw error;
|
|
48
|
-
// Resolve existing ancestors even when the credential directory has not
|
|
49
|
-
// been created yet (macOS /var aliases, Windows short paths, symlinks).
|
|
50
|
-
missing.unshift(path.basename(existing));
|
|
51
|
-
existing = parent;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
39
|
export function parseCloudArgs(command, argv) {
|
|
57
40
|
const options = { directory: '.', timeout: 600 };
|
|
58
41
|
const flags = new Map([
|
|
@@ -582,7 +565,7 @@ export async function cloudCommand(
|
|
|
582
565
|
try {
|
|
583
566
|
if (['deploy', 'update'].includes(command)) {
|
|
584
567
|
const requestedHome = path.dirname(credentialPath(origin, env));
|
|
585
|
-
const actualHome = await
|
|
568
|
+
const actualHome = await prospectivePath(requestedHome);
|
|
586
569
|
const credentialsRelative = path.relative(directory, actualHome);
|
|
587
570
|
if (
|
|
588
571
|
!credentialsRelative ||
|
package/runtime/cloud/state.mjs
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
|
-
import { mkdir, lstat, readFile, writeFile, rm, chmod } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, lstat, readFile, realpath, writeFile, rm, chmod } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { createHash, randomBytes } from 'node:crypto';
|
|
5
5
|
import { rename } from '../fs.mjs';
|
|
6
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
|
+
|
|
7
22
|
export function cloudOrigin(value = 'https://app.prnext.dev') {
|
|
8
23
|
let url;
|
|
9
24
|
try {
|
|
Binary file
|