@thomas.f/prnext 0.1.0-alpha.1 → 0.1.11-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/LICENSE +21 -0
- package/README.md +158 -21
- package/build/app-static.mjs +2 -0
- package/build/compiler.mjs +2 -1
- package/build/dev.mjs +1 -0
- package/build/index.mjs +2 -1
- package/build/inspect.mjs +68 -0
- package/build/loader-cache.mjs +2 -1
- package/build/migrate.mjs +19 -8
- package/build/migration-audit.mjs +126 -0
- package/build/precompress.mjs +2 -1
- package/build/standalone-runner.mjs +1 -1
- package/build/standalone.mjs +80 -9
- package/build/static-export.mjs +2 -1
- package/cli.mjs +100 -14
- package/compat/cache-handlers.cjs +3 -1
- package/compat/data-cache.cjs +10 -1
- package/compat/diagnostics.cjs +40 -0
- package/compat/static-generation.cjs +2 -0
- package/native/npm.mjs +29 -0
- package/native/platforms.json +3 -1
- package/native/resolve.mjs +4 -3
- package/package.json +8 -5
- package/release/verify.mjs +2 -2
- package/runtime/fetch-cache.mjs +12 -1
- package/runtime/fs.mjs +15 -0
- package/runtime/persistent/client.mjs +164 -0
- package/runtime/persistent/common.mjs +67 -0
- package/runtime/persistent/daemon.mjs +312 -0
- package/runtime/persistent/logger.mjs +52 -0
- package/runtime/persistent/startup.mjs +71 -0
- package/runtime/persistent/watchdog.mjs +109 -0
- package/runtime/watch-project.mjs +13 -6
package/build/standalone.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { nodeFileTrace } from '@vercel/nft';
|
|
2
2
|
import resolver from '@vercel/nft/out/resolve-dependency.js';
|
|
3
3
|
import picomatch from 'picomatch';
|
|
4
|
-
import { mkdir, readFile, writeFile, readdir, realpath, readlink, stat, lstat, copyFile, chmod, symlink } from 'node:fs/promises';
|
|
4
|
+
import { mkdir, readFile, writeFile, readdir, realpath, readlink, stat, lstat, copyFile, chmod, symlink, cp } from 'node:fs/promises';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { resolveNativeBinary } from '../native/resolve.mjs';
|
|
7
7
|
import { createHash } from 'node:crypto';
|
|
@@ -13,6 +13,15 @@ const missing = error => error.code === 'ENOENT' || error.code === 'ENOTDIR';
|
|
|
13
13
|
const maxFiles = 100_000;
|
|
14
14
|
const maxBytes = 2 * 1024 ** 3;
|
|
15
15
|
|
|
16
|
+
export async function resolveTraceDependency(...args) {
|
|
17
|
+
const result = await resolver.default(...args);
|
|
18
|
+
// NFT can return mixed separators for scoped packages on Windows. Its
|
|
19
|
+
// realpath traversal only normalizes paths on the tracing base's drive;
|
|
20
|
+
// other drives would then get the wrong package.json boundary.
|
|
21
|
+
const normalize = file => file.startsWith('node:') ? file : path.normalize(file);
|
|
22
|
+
return Array.isArray(result) ? result.map(normalize) : normalize(result);
|
|
23
|
+
}
|
|
24
|
+
|
|
16
25
|
function patternsFor(rules, route) {
|
|
17
26
|
return Object.entries(rules || {}).flatMap(([key, values]) => picomatch(key, { dot: true })(route) ? values : []);
|
|
18
27
|
}
|
|
@@ -66,9 +75,20 @@ export async function createStandalone({ projectRoot, stage, manifest, config })
|
|
|
66
75
|
const warnings = new Set();
|
|
67
76
|
let omittedWarnings = 0;
|
|
68
77
|
const cache = {};
|
|
78
|
+
const traceLocations = new Map();
|
|
69
79
|
let traceReads = 0;
|
|
70
80
|
let traceBytes = 0;
|
|
71
81
|
|
|
82
|
+
function traceLocation(file) {
|
|
83
|
+
if (!traceLocations.has(file)) traceLocations.set(file, (async () => {
|
|
84
|
+
// Expand Windows 8.3 names and filesystem casing, preserving the final
|
|
85
|
+
// symlink itself so workspace links can still be relocated portably.
|
|
86
|
+
if ((await lstat(file)).isSymbolicLink()) return path.join(await realpath(path.dirname(file)), path.basename(file));
|
|
87
|
+
return realpath(file);
|
|
88
|
+
})());
|
|
89
|
+
return traceLocations.get(file);
|
|
90
|
+
}
|
|
91
|
+
|
|
72
92
|
function relocated(file) {
|
|
73
93
|
if (within(stage, file)) return path.join(appOutput, config.distDir, path.relative(stage, file));
|
|
74
94
|
if (within(tracingRoot, file)) return path.join(output, 'app', path.relative(tracingRoot, file));
|
|
@@ -109,19 +129,19 @@ export async function createStandalone({ projectRoot, stage, manifest, config })
|
|
|
109
129
|
return readFile(file);
|
|
110
130
|
},
|
|
111
131
|
async resolve(specifier, parent, job, cjs) {
|
|
112
|
-
const resolved = await
|
|
132
|
+
const resolved = await resolveTraceDependency(specifier, parent, job, cjs);
|
|
113
133
|
if (!specifier.startsWith('.') && !specifier.startsWith('#') && !path.isAbsolute(specifier) && !specifier.startsWith('node:')) {
|
|
114
134
|
const name = specifier.split('/').slice(0, specifier.startsWith('@') ? 2 : 1).join('/');
|
|
115
135
|
for (const file of Array.isArray(resolved) ? resolved : [resolved]) {
|
|
116
136
|
if (file.startsWith('node:')) continue;
|
|
117
137
|
const owner = await packageRoot(file, name);
|
|
118
|
-
if (owner) bridges.set(path.join(path.dirname(parent), 'node_modules', name), owner);
|
|
138
|
+
if (owner) bridges.set(path.join(path.dirname(await realpath(parent)), 'node_modules', name), await realpath(owner));
|
|
119
139
|
}
|
|
120
140
|
}
|
|
121
141
|
return resolved;
|
|
122
142
|
},
|
|
123
143
|
});
|
|
124
|
-
for (const file of result.fileList) traceFiles.add(path.resolve(path.parse(projectRoot).root, file));
|
|
144
|
+
for (const file of result.fileList) traceFiles.add(await traceLocation(path.resolve(path.parse(projectRoot).root, file)));
|
|
125
145
|
for (const warning of result.warnings) {
|
|
126
146
|
const message = warning.message.replaceAll(projectRoot, '<project>').slice(0, 2048);
|
|
127
147
|
if (warnings.has(message)) continue;
|
|
@@ -185,8 +205,14 @@ export async function createStandalone({ projectRoot, stage, manifest, config })
|
|
|
185
205
|
let copiedFiles = 0;
|
|
186
206
|
const pendingLinks = [];
|
|
187
207
|
for (const source of traceFiles) {
|
|
188
|
-
const target = relocated(source);
|
|
189
208
|
const info = await lstat(source);
|
|
209
|
+
// NFT also emits ancestor aliases used by absolute workspace links, such as
|
|
210
|
+
// macOS /var -> /private/var. Their canonical children are already traced;
|
|
211
|
+
// these host directory aliases are not dependencies of the artifact.
|
|
212
|
+
// Real files and links to unrelated external workspaces still go through
|
|
213
|
+
// relocated(), which enforces the configured tracing boundary.
|
|
214
|
+
if (info.isSymbolicLink() && !within(tracingRoot, source) && within(await realpath(source), tracingRoot)) continue;
|
|
215
|
+
const target = relocated(source);
|
|
190
216
|
if (++copiedFiles > maxFiles || (copiedBytes += info.size) > maxBytes) throw new Error('Standalone output exceeds 100000 files / 2 GiB. Narrow tracing includes.');
|
|
191
217
|
if (info.isSymbolicLink()) {
|
|
192
218
|
const destination = await realpath(source);
|
|
@@ -203,9 +229,35 @@ export async function createStandalone({ projectRoot, stage, manifest, config })
|
|
|
203
229
|
for (const [source, owner] of bridges) {
|
|
204
230
|
// An excluded optional dependency must not be brought back by a bridge.
|
|
205
231
|
if (!includedOwners.has(owner)) continue;
|
|
206
|
-
|
|
232
|
+
const marker = `${path.sep}node_modules${path.sep}`;
|
|
233
|
+
const offset = source.lastIndexOf(marker);
|
|
234
|
+
pendingLinks.push([relocated(source), relocated(owner), source.slice(offset + marker.length), relocated(source.slice(0, offset))]);
|
|
207
235
|
}
|
|
208
|
-
|
|
236
|
+
const materialized = new Map();
|
|
237
|
+
const dependencyLinks = pendingLinks.filter(([, , packageName]) => packageName);
|
|
238
|
+
function original(file) {
|
|
239
|
+
for (const [copy, source] of materialized) {
|
|
240
|
+
if (within(copy, file)) return original(path.join(source, path.relative(copy, file)));
|
|
241
|
+
}
|
|
242
|
+
return file;
|
|
243
|
+
}
|
|
244
|
+
for (let [link, target, packageName, importer] of pendingLinks) {
|
|
245
|
+
if (process.platform === 'win32' && packageName) {
|
|
246
|
+
// Reuse Node's ancestor lookup before copying a bridge. Install shared
|
|
247
|
+
// hoisted dependencies once, keeping React and package singletons intact.
|
|
248
|
+
const search = createRequire(path.join(importer, '__prnext_trace.cjs')).resolve.paths(packageName)
|
|
249
|
+
.map(directory => path.join(directory, packageName)).filter(file => within(output, file));
|
|
250
|
+
const vacant = [];
|
|
251
|
+
let found;
|
|
252
|
+
for (const candidate of search) {
|
|
253
|
+
try { await lstat(candidate); found = candidate; break; }
|
|
254
|
+
catch (error) { if (!missing(error)) throw error; vacant.push(candidate); }
|
|
255
|
+
}
|
|
256
|
+
if (found && original(found) === original(target)) continue;
|
|
257
|
+
// If a nearer version shadows this owner, add the bridge before it.
|
|
258
|
+
link = found ? vacant[0] : vacant.at(-1);
|
|
259
|
+
if (!link) throw new Error(`Conflicting standalone dependency ${packageName} at ${importer}.`);
|
|
260
|
+
}
|
|
209
261
|
if (link === target) continue;
|
|
210
262
|
await mkdir(path.dirname(link), { recursive: true });
|
|
211
263
|
try {
|
|
@@ -214,7 +266,26 @@ export async function createStandalone({ projectRoot, stage, manifest, config })
|
|
|
214
266
|
if (current.isSymbolicLink() && path.resolve(path.dirname(link), await readlink(link)) === target) continue;
|
|
215
267
|
throw new Error(`Conflicting standalone dependency at ${path.relative(output, link)}.`);
|
|
216
268
|
} catch (error) { if (!missing(error)) throw error; }
|
|
217
|
-
|
|
269
|
+
if (process.platform === 'win32') {
|
|
270
|
+
// Junctions contain absolute targets and break when an artifact moves;
|
|
271
|
+
// symlinks require privileges. Copy only the already traced files.
|
|
272
|
+
await cp(target, link, { recursive: true, dereference: true, async filter(source) {
|
|
273
|
+
const info = await stat(source);
|
|
274
|
+
if (info.isFile() && (++copiedFiles > maxFiles || (copiedBytes += info.size) > maxBytes)) throw new Error('Standalone output exceeds 100000 files / 2 GiB. Narrow tracing includes.');
|
|
275
|
+
return true;
|
|
276
|
+
} });
|
|
277
|
+
materialized.set(link, target);
|
|
278
|
+
// A copied workspace package no longer resolves through its real path.
|
|
279
|
+
// Give imports inside that copy the same dependency owners as the source.
|
|
280
|
+
for (const [dependency, owner, name, parent] of dependencyLinks) {
|
|
281
|
+
if (within(target, parent)) pendingLinks.push([
|
|
282
|
+
path.join(link, path.relative(target, dependency)), owner, name,
|
|
283
|
+
path.join(link, path.relative(target, parent)),
|
|
284
|
+
]);
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
await symlink(path.relative(path.dirname(link), target), link, (await stat(target)).isDirectory() ? 'dir' : 'file');
|
|
288
|
+
}
|
|
218
289
|
}
|
|
219
290
|
const binaryName = process.platform === 'win32' ? 'prnext.exe' : 'prnext';
|
|
220
291
|
const binary = await resolveNativeBinary();
|
|
@@ -230,7 +301,7 @@ export async function createStandalone({ projectRoot, stage, manifest, config })
|
|
|
230
301
|
await writeFile(path.join(appOutput, 'package.json'), packageJson);
|
|
231
302
|
const relativeApp = slash(path.relative(output, appOutput));
|
|
232
303
|
await writeFile(path.join(output, 'package.json'), '{"private":true,"type":"commonjs"}\n');
|
|
233
|
-
await writeFile(path.join(output, 'server.js'), `'use strict';\nconst path=require('node:path');const {spawn}=require('node:child_process');\nconst root=path.join(__dirname,${JSON.stringify(relativeApp)});\nconst child=spawn(path.join(__dirname,'bin',${JSON.stringify(binaryName)}),['start',root,'--hostname',process.env.HOSTNAME||'0.0.0.0','--port',process.env.PORT||'3000','--workers',process.env.PRNEXT_WORKERS||'1','--node',process.execPath],{cwd:root,stdio:'inherit',env:{...process.env,NODE_ENV:'production'}});\nfor(const signal of ['SIGINT','SIGTERM'])process.on(signal,()=>child.kill(signal));\nchild.on('error',error=>{console.error(error.message);process.exitCode=1;});child.on('exit',(code,signal)=>{process.exitCode=code??(signal==='SIGINT'?130:signal==='SIGTERM'?143:1);});\n`);
|
|
304
|
+
await writeFile(path.join(output, 'server.js'), `'use strict';\nconst path=require('node:path');const {spawn}=require('node:child_process');\nconst root=path.join(__dirname,${JSON.stringify(relativeApp)});\nconst child=spawn(path.join(__dirname,'bin',${JSON.stringify(binaryName)}),['start',root,'--hostname',process.env.HOSTNAME||'0.0.0.0','--port',process.env.PORT||'3000','--workers',process.env.PRNEXT_WORKERS||'1','--node',process.execPath,...(process.platform==='win32'?['--shutdown-on-stdin-eof']:[])],{cwd:root,stdio:process.platform==='win32'?['pipe','inherit','inherit']:'inherit',windowsHide:true,env:{...process.env,NODE_ENV:'production'}});\nfor(const signal of ['SIGINT','SIGTERM'])process.on(signal,()=>{if(process.platform==='win32')child.stdin.end();else child.kill(signal)});\nchild.on('error',error=>{console.error(error.message);process.exitCode=1;});child.on('exit',(code,signal)=>{process.exitCode=code??(signal==='SIGINT'?130:signal==='SIGTERM'?143:1);});\n`);
|
|
234
305
|
if (process.platform !== 'win32') {
|
|
235
306
|
const quotedApp = "'" + relativeApp.replaceAll("'", "'\\''") + "'";
|
|
236
307
|
await writeFile(path.join(output, 'start'), `#!/bin/sh\nset -eu\nSELF=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)\ncd "$SELF/"${quotedApp}\nexec "$SELF/bin/prnext" start . --hostname "\${HOSTNAME:-0.0.0.0}" --port "\${PORT:-3000}" --workers "\${PRNEXT_WORKERS:-1}" --node "\${PRNEXT_NODE:-node}" "$@"\n`);
|
package/build/static-export.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { rename } from '../runtime/fs.mjs';
|
|
2
|
+
import { mkdir, readFile, writeFile, cp, readdir, lstat, rm } from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { randomUUID } from 'node:crypto';
|
|
4
5
|
|
package/cli.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { rename } from './runtime/fs.mjs';
|
|
2
3
|
import { spawn } from 'node:child_process';
|
|
3
|
-
import { readFile, writeFile,
|
|
4
|
+
import { readFile, writeFile, rm } from 'node:fs/promises';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
import { resolveNativeBinary, packageManifest } from './native/resolve.mjs';
|
|
@@ -11,20 +12,34 @@ const self = fileURLToPath(import.meta.url);
|
|
|
11
12
|
const [command = 'help', ...argv] = process.argv.slice(2);
|
|
12
13
|
const help = `PRNext ${packageManifest.version}
|
|
13
14
|
|
|
14
|
-
Use prnext
|
|
15
|
+
Use prnext for portable scripts; prn is a shortcut on compatible shells.
|
|
15
16
|
|
|
16
|
-
prnext build [directory]
|
|
17
|
-
prnext start [directory] [options]
|
|
18
|
-
prnext
|
|
19
|
-
prnext
|
|
20
|
-
prnext
|
|
21
|
-
|
|
17
|
+
prnext build [directory] Build React/TypeScript and static HTML
|
|
18
|
+
prnext start [directory] [options] Start native Rust production server
|
|
19
|
+
prnext pstart [directory] [options] Start production persistently in the background
|
|
20
|
+
prnext prestart [name] Replace a persistent app after a health check
|
|
21
|
+
prnext pstop [name] [--all] Stop persistent apps
|
|
22
|
+
prnext pstatus [--json] Show persistent app status (alias: plist)
|
|
23
|
+
prnext plogs [name] [--follow] Read persistent app logs
|
|
24
|
+
prnext pstartup [--remove] Configure restoration at user login
|
|
25
|
+
prnext pstart --help All persistent commands and options
|
|
26
|
+
prnext dev [directory] [options] Rebuild and restart on source changes
|
|
27
|
+
prnext routes [directory] Display compiled routes
|
|
28
|
+
prnext check [directory] [--json] Check an existing Next project before building
|
|
29
|
+
prnext inspect [directory] [--json] Explain routes, cache decisions and timings
|
|
30
|
+
prnext host [prnext.host.json] Host built apps by hostname; wake on demand
|
|
31
|
+
prnext host [config] --check Validate hosting configuration and builds
|
|
32
|
+
prnext host [config] --status [--json] Show memory, process and idle status
|
|
33
|
+
prnext migrate [directory] [options] Migrate a Next project's scripts and dependencies
|
|
22
34
|
|
|
23
35
|
Migration options: --dry-run (preview), --no-install (prepare only), --json
|
|
36
|
+
prnext migrate --check Audit source compatibility without migrating
|
|
37
|
+
prnext migrate --check --against URL --candidate URL [--routes paths.json]
|
|
38
|
+
Compare GET responses before switching
|
|
24
39
|
Migration keeps Next.js and backs up package.json and existing lockfiles.
|
|
25
|
-
Simple next dev/build/start scripts become
|
|
40
|
+
Simple next dev/build/start scripts become prnext commands; custom shell scripts need a manual edit.
|
|
26
41
|
|
|
27
|
-
Server options: --port 3000 --hostname 127.0.0.1 --workers 1
|
|
42
|
+
Server options: --port 3000 --hostname 127.0.0.1 --workers 1 --inspect
|
|
28
43
|
Production profiles: --profile balanced|speed|memory|classic
|
|
29
44
|
Memory favors lower RAM under concurrency, accepting more waiting and CPU per response.
|
|
30
45
|
Without an option: PRNEXT_PROFILE, legacy PRNEXT_MEMORY_PROFILE=compact, then balanced.
|
|
@@ -45,17 +60,68 @@ function stop(child) {
|
|
|
45
60
|
return new Promise(resolve => {
|
|
46
61
|
const timer = setTimeout(() => child.kill('SIGKILL'), 5000);
|
|
47
62
|
child.once('exit', () => { clearTimeout(timer); resolve(); });
|
|
48
|
-
child.
|
|
63
|
+
if (child.stdin) child.stdin.end();
|
|
64
|
+
else child.kill('SIGTERM');
|
|
49
65
|
});
|
|
50
66
|
}
|
|
51
67
|
|
|
52
68
|
async function main() {
|
|
53
69
|
if (['help', '--help', '-h'].includes(command)) { console.log(help); return; }
|
|
54
70
|
if (['--version', '-v'].includes(command)) { console.log(packageManifest.version); return; }
|
|
71
|
+
if (['pstart', 'prestart', 'pstop', 'pstatus', 'plist', 'plogs', 'pdelete', 'pstartup', 'pdown'].includes(command)) {
|
|
72
|
+
const { persistentCommand } = await import('./runtime/persistent/client.mjs');
|
|
73
|
+
await persistentCommand(command, argv);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (command === 'host') {
|
|
77
|
+
if (argv.includes('--help') || argv.includes('-h')) { console.log(help); return; }
|
|
78
|
+
const files = argv.filter(arg => !arg.startsWith('-'));
|
|
79
|
+
if (files.length > 1 || argv.some(arg => arg.startsWith('-') && !['--status', '--json', '--check'].includes(arg))) throw new Error('host accepts [config], --check, or --status [--json]');
|
|
80
|
+
const config = path.resolve(files[0] || 'prnext.host.json');
|
|
81
|
+
if (argv.includes('--status') && argv.includes('--check')) throw new Error('Choose --check or --status for the host command.');
|
|
82
|
+
if (argv.includes('--status')) {
|
|
83
|
+
const info = path.parse(config);
|
|
84
|
+
const report = JSON.parse(await readFile(path.join(info.dir, info.name + '.status.json'), 'utf8'));
|
|
85
|
+
report.snapshotAgeMs = Date.now() - report.time;
|
|
86
|
+
if (argv.includes('--json')) console.log(JSON.stringify(report, null, 2));
|
|
87
|
+
else {
|
|
88
|
+
console.log(`PRNext host: ${report.reservedMb}/${report.memoryMb} MiB reserved; snapshot ${Math.round(report.snapshotAgeMs / 1000)}s ago${report.stopping ? ' (stopped)' : ''}`);
|
|
89
|
+
for (const app of report.apps) console.log(`${app.name}: ${app.state}, ${(app.rssBytes / 1024 / 1024).toFixed(1)}/${app.memoryMb} MiB RSS, ${app.activeRequests} active request(s), ${app.starts} start(s) — ${app.reason}`);
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (argv.includes('--json')) throw new Error('--json requires --status');
|
|
94
|
+
const binary = await resolveNativeBinary();
|
|
95
|
+
const child = spawn(binary, ['host', config, '--node', process.execPath, '--shutdown-on-stdin-eof', ...(argv.includes('--check') ? ['--check'] : [])], { stdio: ['pipe', 'inherit', 'inherit'], windowsHide: true });
|
|
96
|
+
const shutdown = () => child.stdin.end();
|
|
97
|
+
process.once('SIGINT', shutdown); process.once('SIGTERM', shutdown);
|
|
98
|
+
await done(child);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
55
101
|
if (command === 'migrate') {
|
|
56
102
|
if (argv.includes('--help') || argv.includes('-h')) { console.log(help); return; }
|
|
57
103
|
const { parseMigrationArgs, migrateProject } = await import('./build/migrate.mjs');
|
|
58
104
|
const { directory, options } = parseMigrationArgs(argv);
|
|
105
|
+
if (options.check) {
|
|
106
|
+
const { auditMigration, compareServers } = await import('./build/migration-audit.mjs');
|
|
107
|
+
const report = await auditMigration(directory);
|
|
108
|
+
if (options.against) {
|
|
109
|
+
const routes = options.routes ? JSON.parse(await readFile(path.resolve(options.routes), 'utf8'))
|
|
110
|
+
: report.routes.filter(route => route.kind === 'page' && !route.pattern.includes('[')).map(route => route.pattern);
|
|
111
|
+
report.comparison = await compareServers({ ...options, routes });
|
|
112
|
+
report.ok &&= report.comparison.ok;
|
|
113
|
+
}
|
|
114
|
+
if (options.json) console.log(JSON.stringify(report, null, 2));
|
|
115
|
+
else {
|
|
116
|
+
console.log(`PRNext migration check ${report.ok ? 'passed' : 'failed'}: ${directory}`);
|
|
117
|
+
console.log(`${report.files} source files; ${report.routes.length} routes.`);
|
|
118
|
+
for (const item of report.findings) console.log(`${item.severity.toUpperCase()} ${item.file || 'project'}${item.line ? ':' + item.line : ''}: ${item.message}\n ${item.action}`);
|
|
119
|
+
for (const result of report.comparison?.results || []) console.log(`${result.equal ? 'MATCH' : 'DIFFERENT'} ${result.route}: ${result.error || result.differences.join(', ') || 'responses match'}`);
|
|
120
|
+
for (const note of [...report.notes, ...(report.comparison ? [report.comparison.scope] : [])]) console.log(note);
|
|
121
|
+
}
|
|
122
|
+
if (!report.ok) process.exitCode = 1;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
59
125
|
const report = await migrateProject(directory, options);
|
|
60
126
|
if (options.json) console.log(JSON.stringify(report, null, 2));
|
|
61
127
|
else {
|
|
@@ -64,14 +130,25 @@ async function main() {
|
|
|
64
130
|
for (const backup of report.backups) console.log(`Backup: ${backup}`);
|
|
65
131
|
for (const note of report.notes) console.log(note);
|
|
66
132
|
if (report.error) console.error(report.error);
|
|
67
|
-
if (report.ok && !options.dryRun) console.log(options.install ? 'Next: npm run build, then npm start (or npm run dev).' : 'Next: install dependencies with your package manager, then
|
|
133
|
+
if (report.ok && !options.dryRun) console.log(options.install ? 'Next: npm run build, then npm start (or npm run dev).' : 'Next: install dependencies with your package manager, then prnext check and prnext build.');
|
|
68
134
|
}
|
|
69
135
|
if (!report.ok) process.exitCode = 1;
|
|
70
136
|
return;
|
|
71
137
|
}
|
|
72
|
-
if (!['build', 'start', 'dev', 'routes', 'check'].includes(command)) throw new Error(`Unknown command: ${command}\n${help}`);
|
|
138
|
+
if (!['build', 'start', 'dev', 'routes', 'check', 'inspect'].includes(command)) throw new Error(`Unknown command: ${command}\n${help}`);
|
|
73
139
|
const args = [...argv];
|
|
74
140
|
const root = path.resolve(args[0] && !args[0].startsWith('-') ? args.shift() : '.');
|
|
141
|
+
if (command === 'inspect') {
|
|
142
|
+
if (args.some(arg => arg !== '--json')) throw new Error('inspect accepts only [directory] and --json');
|
|
143
|
+
const { inspectProject, printInspection } = await import('./build/inspect.mjs');
|
|
144
|
+
const report = await inspectProject(root);
|
|
145
|
+
if (args.includes('--json')) console.log(JSON.stringify(report, null, 2)); else printInspection(report);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (['start', 'dev'].includes(command) && args.includes('--inspect')) {
|
|
149
|
+
args.splice(args.indexOf('--inspect'), 1);
|
|
150
|
+
process.env.PRNEXT_INSPECT_DIR = path.join(root, '.prnext-cache/inspect');
|
|
151
|
+
}
|
|
75
152
|
if (command === 'build' || command === 'dev') {
|
|
76
153
|
// Build plugins such as Contentlayer read cwd/INIT_CWD rather than webpack's
|
|
77
154
|
// context. Explicit application directories must behave like `cd app`.
|
|
@@ -107,7 +184,8 @@ async function main() {
|
|
|
107
184
|
const binary = await resolveNativeBinary();
|
|
108
185
|
let outputDirectory = await readBuildDirectory(root);
|
|
109
186
|
const launch = () => spawn(binary, [command === 'routes' ? 'routes' : 'start', root,
|
|
110
|
-
...(command === 'routes' ? [] : ['--worker', path.join(root, outputDirectory, 'runtime/worker.mjs')
|
|
187
|
+
...(command === 'routes' ? [] : ['--worker', path.join(root, outputDirectory, 'runtime/worker.mjs'), '--node', process.execPath,
|
|
188
|
+
'--shutdown-on-stdin-eof']), ...args], { stdio: ['pipe', 'inherit', 'inherit'], windowsHide: true, env: { ...process.env, NODE_ENV: process.env.NODE_ENV || (command === 'dev' ? 'development' : 'production') } });
|
|
111
189
|
let server;
|
|
112
190
|
let stopping = false;
|
|
113
191
|
let watcher;
|
|
@@ -149,11 +227,13 @@ async function main() {
|
|
|
149
227
|
let buildOutput = '';
|
|
150
228
|
try {
|
|
151
229
|
const actualChanges = await filterChanges(pending);
|
|
230
|
+
if (stopping) return;
|
|
152
231
|
if (attempted && !actualChanges.length) return;
|
|
153
232
|
attempted = true;
|
|
154
233
|
const changed = actualChanges.slice(0, 32).map(file => file.slice(0, 256));
|
|
155
234
|
await notifyDev({ state: 'building' });
|
|
156
235
|
if (actualChanges.some(file => /(?:^|\/)(?:(?:next|prnext|contentlayer)\.config\.|package(?:-lock)?\.json$|(?:pnpm-lock\.yaml|yarn\.lock)$)/.test(file))) { await stop(rebuildChild); rebuildChild = undefined; }
|
|
236
|
+
if (stopping) return;
|
|
157
237
|
if (!rebuildChild || rebuildChild.exitCode !== null || rebuildChild.signalCode) {
|
|
158
238
|
rebuildChild = spawn(process.execPath, [path.join(path.dirname(self), 'build/dev-worker.mjs')], { stdio: ['inherit', 'pipe', 'pipe', 'ipc'], env: { ...process.env, NODE_ENV: process.env.NODE_ENV || 'development' } });
|
|
159
239
|
}
|
|
@@ -179,6 +259,9 @@ async function main() {
|
|
|
179
259
|
await stop(server);
|
|
180
260
|
outputDirectory = await readBuildDirectory(root);
|
|
181
261
|
await watcher.refresh();
|
|
262
|
+
// Shutdown can arrive while the previous server is draining. Never
|
|
263
|
+
// leave a replacement running after shutdown already stopped its peers.
|
|
264
|
+
if (stopping) return;
|
|
182
265
|
for (const file of changedFiles) {
|
|
183
266
|
if (file === outputDirectory || file.startsWith(outputDirectory + '/') || outputDirectory.startsWith(file + '/')) changedFiles.delete(file);
|
|
184
267
|
}
|
|
@@ -186,6 +269,7 @@ async function main() {
|
|
|
186
269
|
server = launch();
|
|
187
270
|
server.on('error', error => console.error(`PRNext server: ${error.message}`));
|
|
188
271
|
const manifest = JSON.parse(await readFile(path.join(root, outputDirectory, 'manifest.json'), 'utf8'));
|
|
272
|
+
if (stopping) return;
|
|
189
273
|
await notifyDev({ state: 'ready', buildId: manifest.buildId, clientManifest: manifest.devClient, changed });
|
|
190
274
|
console.log('Source compiled. React Fast Refresh applied in connected browsers.');
|
|
191
275
|
}
|
|
@@ -197,6 +281,7 @@ async function main() {
|
|
|
197
281
|
ignore: relative => relative === outputDirectory || relative.startsWith(outputDirectory + '/'),
|
|
198
282
|
onError: error => { console.error(`PRNext development watcher: ${error.message}`); process.exitCode = 1; void shutdown(); },
|
|
199
283
|
onChange: filename => {
|
|
284
|
+
if (stopping) return;
|
|
200
285
|
const relative = String(filename || '').replaceAll(path.sep, '/');
|
|
201
286
|
if (relative === outputDirectory || relative.startsWith(outputDirectory + '/') || !shouldWatchProjectFile(filename)) return;
|
|
202
287
|
changedFiles.add(String(filename).replaceAll(path.sep, '/'));
|
|
@@ -204,6 +289,7 @@ async function main() {
|
|
|
204
289
|
timer = setTimeout(() => { if (changedFiles.size) void rebuild(); }, 150);
|
|
205
290
|
},
|
|
206
291
|
});
|
|
292
|
+
if (stopping) { watcher.close(); return; }
|
|
207
293
|
await rebuild();
|
|
208
294
|
}
|
|
209
295
|
|
|
@@ -134,7 +134,9 @@ async function customCachedValue(key, kind, producer, options) {
|
|
|
134
134
|
let work = joined?.work;
|
|
135
135
|
if (!work) {
|
|
136
136
|
const revision = revisions.get(handler) || 0;
|
|
137
|
-
|
|
137
|
+
// Handler invalidations use wall-clock timestamps. On Windows the
|
|
138
|
+
// performance clock can run ahead of Date.now(), hiding a later update.
|
|
139
|
+
const timestamp = Date.now();
|
|
138
140
|
const record = { revision, timestamp, work: undefined, metadata: undefined };
|
|
139
141
|
work = (async () => {
|
|
140
142
|
const result = await producer();
|
package/compat/data-cache.cjs
CHANGED
|
@@ -3,6 +3,7 @@ const { AsyncLocalStorage, AsyncResource } = require('node:async_hooks');
|
|
|
3
3
|
const http = require('node:http');
|
|
4
4
|
const { setTimeout: delay } = require('node:timers/promises');
|
|
5
5
|
const { trackStaticDependency } = require('./static-generation.cjs');
|
|
6
|
+
const diagnostics = require('./diagnostics.cjs');
|
|
6
7
|
|
|
7
8
|
// Capture before the runtime installs its application-facing fetch wrapper.
|
|
8
9
|
const nativeFetchSymbol = Symbol.for('prnext.nativeFetch');
|
|
@@ -309,12 +310,16 @@ async function cachedValue(key, producer, options = {}) {
|
|
|
309
310
|
if (state) await state.invalidations;
|
|
310
311
|
throwIfAborted(signal);
|
|
311
312
|
if (options.incremental && (context?.cacheHandler || context?.manifest?.config?.cacheHandler)) {
|
|
313
|
+
if (diagnostics.enabled) diagnostics.record('cache', { route: diagnostics.route(context), state: 'custom-handler', reason: 'delegated to incremental cache handler' });
|
|
312
314
|
return require('./incremental-cache.cjs').cachedIncrementalValue(key, producer, { ...options, context, tags, paths, revalidate });
|
|
313
315
|
}
|
|
314
316
|
const target = endpoint();
|
|
315
317
|
// Builds and standalone runtime use have no native cache server. Compute
|
|
316
318
|
// without claiming persistence; production workers always receive its URL.
|
|
317
|
-
if (!target)
|
|
319
|
+
if (!target) {
|
|
320
|
+
if (diagnostics.enabled) diagnostics.record('cache', { route: diagnostics.route(context), state: 'bypass', reason: 'native cache endpoint unavailable' });
|
|
321
|
+
return uncachedValue(producer, state, signal);
|
|
322
|
+
}
|
|
318
323
|
const deadline = Date.now() + WAIT_TIMEOUT_MS;
|
|
319
324
|
while (true) {
|
|
320
325
|
throwIfAborted(signal);
|
|
@@ -328,6 +333,8 @@ async function cachedValue(key, producer, options = {}) {
|
|
|
328
333
|
}
|
|
329
334
|
if (options.versioned && (typeof entry.key !== 'string' || !/^[a-f\d]{64}$/.test(entry.key) || !Number.isSafeInteger(entry.generation) || entry.generation < 0)) throw new Error('Invalid PRNext versioned cache response');
|
|
330
335
|
const entryKey = options.versioned ? entry.key : key;
|
|
336
|
+
if (diagnostics.enabled && entry.state !== 'pending') diagnostics.record('cache', { route: diagnostics.route(context), state: entry.state,
|
|
337
|
+
key: key.slice(0, 12), tags: tags.map(diagnostics.label), paths: paths.map(diagnostics.label) });
|
|
331
338
|
const produce = options.versioned ? info => producer({ ...info, generation: entry.generation }) : producer;
|
|
332
339
|
if (entry.state === 'fresh') return decodeValue(entry.value);
|
|
333
340
|
if (entry.state === 'stale') {
|
|
@@ -364,6 +371,8 @@ function queueInvalidation(context, operation) {
|
|
|
364
371
|
if (target) await rpc(target, { op: 'invalidate', ...operation });
|
|
365
372
|
if (custom) await require('./cache-handlers.cjs').invalidateHandlers(context, operation);
|
|
366
373
|
if (incremental) await require('./incremental-cache.cjs').invalidate(context, operation);
|
|
374
|
+
if (diagnostics.enabled) diagnostics.record('invalidation', { route: diagnostics.route(context), mode: operation.mode,
|
|
375
|
+
tags: (operation.tags || []).map(diagnostics.label), paths: (operation.paths || []).map(diagnostics.label) });
|
|
367
376
|
});
|
|
368
377
|
// Public invalidation functions return void. Retain errors for the next read
|
|
369
378
|
// or runtime flush without producing an unhandled rejection in the meantime.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Opt-in, bounded diagnostics. Never retain request headers, cookies or bodies.
|
|
3
|
+
const directory = process.env.PRNEXT_INSPECT_DIR;
|
|
4
|
+
const { threadId } = require('node:worker_threads');
|
|
5
|
+
let queue = [], scheduled = false, writing = false, size;
|
|
6
|
+
const limit = 1024 * 1024;
|
|
7
|
+
function record(type, fields = {}) {
|
|
8
|
+
if (!directory || queue.length >= 256) return;
|
|
9
|
+
const line = JSON.stringify({ time: Date.now(), pid: process.pid, type, ...fields }) + '\n';
|
|
10
|
+
if (Buffer.byteLength(line) > 4096) return;
|
|
11
|
+
queue.push(line);
|
|
12
|
+
if (!scheduled) { scheduled = true; setTimeout(flush, 100).unref(); }
|
|
13
|
+
}
|
|
14
|
+
async function flush() {
|
|
15
|
+
scheduled = false;
|
|
16
|
+
if (writing || !queue.length) return;
|
|
17
|
+
writing = true;
|
|
18
|
+
const fs = require('node:fs/promises'), path = require('node:path');
|
|
19
|
+
const file = path.join(directory, `worker-${process.pid}-${threadId}.jsonl`);
|
|
20
|
+
const batch = queue.join(''); queue = [];
|
|
21
|
+
try {
|
|
22
|
+
await fs.mkdir(directory, { recursive: true });
|
|
23
|
+
if (size === undefined) {
|
|
24
|
+
size = await fs.stat(file).then(value => value.size, () => 0);
|
|
25
|
+
// Retain the most recent worker sessions. Each has at most two 1 MiB files.
|
|
26
|
+
const files = await Promise.all((await fs.readdir(directory)).filter(name => /^worker-\d+-\d+\.jsonl(?:\.1)?$/.test(name)).map(async name => ({ name, modified: await fs.stat(path.join(directory, name)).then(value => value.mtimeMs, () => 0) })));
|
|
27
|
+
for (const old of files.sort((a, b) => b.modified - a.modified).slice(62)) await fs.rm(path.join(directory, old.name), { force: true }).catch(() => {});
|
|
28
|
+
}
|
|
29
|
+
if (size + Buffer.byteLength(batch) > limit) { await fs.rm(file + '.1', { force: true }); await fs.rename(file, file + '.1').catch(error => { if (error.code !== 'ENOENT') throw error; }); size = 0; }
|
|
30
|
+
await fs.appendFile(file, batch, { mode: 0o600 }); size += Buffer.byteLength(batch);
|
|
31
|
+
} catch { /* Diagnostics must never fail an application request. */ }
|
|
32
|
+
finally { writing = false; if (queue.length && !scheduled) { scheduled = true; setTimeout(flush, 100).unref(); } }
|
|
33
|
+
}
|
|
34
|
+
function route(context) { return String(context?.routePattern || '<unattributed>').slice(0, 256); }
|
|
35
|
+
function destination(input) {
|
|
36
|
+
try { const url = new URL(typeof input === 'string' || input instanceof URL ? input : input.url); return (url.origin + url.pathname).slice(0, 256); }
|
|
37
|
+
catch { return '<unknown>'; }
|
|
38
|
+
}
|
|
39
|
+
function label(value) { return require('node:crypto').createHash('sha256').update(String(value)).digest('hex').slice(0, 12); }
|
|
40
|
+
module.exports = { enabled: Boolean(directory), record, route, destination, label, flush };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const {sampleObject} = require('./instant-samples.cjs');
|
|
3
|
+
const diagnostics = require('./diagnostics.cjs');
|
|
3
4
|
let requestReader;
|
|
4
5
|
|
|
5
6
|
function context() {
|
|
@@ -24,6 +25,7 @@ function dynamicUsage(reason, request = context()) {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
function staticBailout(reason, request = context()) {
|
|
28
|
+
if (diagnostics.enabled) diagnostics.record('dynamic', { route: diagnostics.route(request), reason });
|
|
27
29
|
const state = request?.staticState;
|
|
28
30
|
if (!state) return true;
|
|
29
31
|
if (state.partial && state.mode === 'auto') {
|
package/native/npm.mjs
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
|
|
6
|
+
const execute = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
// Execute npm's JS entry point: .cmd shims cannot be execFile'd on Windows.
|
|
9
|
+
// No shell interpolation, including when the project path contains spaces.
|
|
10
|
+
export function npmCommand(env = process.env) {
|
|
11
|
+
const candidates = [env.npm_execpath];
|
|
12
|
+
const directories = [path.dirname(process.execPath), ...(env.PATH || env.Path || '').split(path.delimiter)];
|
|
13
|
+
for (const directory of directories.filter(Boolean)) {
|
|
14
|
+
candidates.push(path.join(directory, 'node_modules/npm/bin/npm-cli.js'));
|
|
15
|
+
try {
|
|
16
|
+
const npm = realpathSync(path.join(directory, process.platform === 'win32' ? 'npm.cmd' : 'npm'));
|
|
17
|
+
candidates.push(npm.endsWith('.js') ? npm : path.join(path.dirname(npm), 'node_modules/npm/bin/npm-cli.js'));
|
|
18
|
+
} catch {}
|
|
19
|
+
}
|
|
20
|
+
const cli = candidates.find(file => file && /(?:^|[/\\])npm-cli\.js$/.test(file) && existsSync(file));
|
|
21
|
+
if (cli) return { command: process.execPath, args: [cli] };
|
|
22
|
+
if (process.platform !== 'win32') return { command: 'npm', args: [] };
|
|
23
|
+
throw new Error('Cannot locate npm-cli.js. Install Node.js with npm, or run this command through npm run.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function executeNpm(args, options = {}) {
|
|
27
|
+
const npm = npmCommand();
|
|
28
|
+
return execute(npm.command, [...npm.args, ...args], { windowsHide: true, ...options });
|
|
29
|
+
}
|
package/native/platforms.json
CHANGED
|
@@ -2,5 +2,7 @@
|
|
|
2
2
|
{ "id": "darwin-arm64", "package": "prnext-darwin-arm64", "os": "darwin", "cpu": "arm64", "target": "aarch64-apple-darwin" },
|
|
3
3
|
{ "id": "darwin-x64", "package": "prnext-darwin-x64", "os": "darwin", "cpu": "x64", "target": "x86_64-apple-darwin" },
|
|
4
4
|
{ "id": "linux-arm64-gnu", "package": "prnext-linux-arm64-gnu", "os": "linux", "cpu": "arm64", "libc": "glibc", "target": "aarch64-unknown-linux-gnu" },
|
|
5
|
-
{ "id": "linux-x64-gnu", "package": "prnext-linux-x64-gnu", "os": "linux", "cpu": "x64", "libc": "glibc", "target": "x86_64-unknown-linux-gnu" }
|
|
5
|
+
{ "id": "linux-x64-gnu", "package": "prnext-linux-x64-gnu", "os": "linux", "cpu": "x64", "libc": "glibc", "target": "x86_64-unknown-linux-gnu" },
|
|
6
|
+
{ "id": "win32-x64-msvc", "package": "@thomas.f/prnext-win32-x64-msvc", "os": "win32", "cpu": "x64", "target": "x86_64-pc-windows-msvc" },
|
|
7
|
+
{ "id": "win32-arm64-msvc", "package": "@thomas.f/prnext-win32-arm64-msvc", "os": "win32", "cpu": "arm64", "target": "aarch64-pc-windows-msvc" }
|
|
6
8
|
]
|
package/native/resolve.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import path from 'node:path';
|
|
|
8
8
|
export const packageRoot = fileURLToPath(new URL('../', import.meta.url));
|
|
9
9
|
export const packageManifest = JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
10
10
|
export const platforms = JSON.parse(readFileSync(new URL('./platforms.json', import.meta.url), 'utf8'));
|
|
11
|
+
export const nativeBinaryName = (platform = process.platform) => platform === 'win32' ? 'prnext.exe' : 'prnext';
|
|
11
12
|
|
|
12
13
|
// A dependency installed in node_modules must never mistake its consumer for
|
|
13
14
|
// the framework checkout, even if that consumer also has a Cargo workspace.
|
|
@@ -27,7 +28,7 @@ export function nativePlatform({ platform = process.platform, arch = process.arc
|
|
|
27
28
|
libc = process.report?.getReport().header.glibcVersionRuntime ? 'glibc' : 'musl';
|
|
28
29
|
}
|
|
29
30
|
const result = platforms.find(item => item.os === platform && item.cpu === arch && (!item.libc || item.libc === libc));
|
|
30
|
-
if (!result) throw new Error(`PRNext has no prebuilt server for ${platform}/${arch}${libc ? `/${libc}` : ''}.
|
|
31
|
+
if (!result) throw new Error(`PRNext has no prebuilt server for ${platform}/${arch}${libc ? `/${libc}` : ''}. Supported targets are Windows (MSVC), macOS and Linux glibc on arm64/x64. Use a supported system, or set PRNEXT_BINARY to your own compatible build.`);
|
|
31
32
|
return result;
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -47,7 +48,7 @@ export async function resolveNativeBinary({ env = process.env, root = packageRoo
|
|
|
47
48
|
}
|
|
48
49
|
const checkout = sourceCheckout(root);
|
|
49
50
|
if (checkout) {
|
|
50
|
-
const binary = path.join(checkout, 'target/release',
|
|
51
|
+
const binary = path.join(checkout, 'target/release', nativeBinaryName());
|
|
51
52
|
try { return await executableFile(binary); }
|
|
52
53
|
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
53
54
|
if (!allowBuild) throw new Error('Native server missing from this checkout. Run npm run build:native.');
|
|
@@ -75,7 +76,7 @@ export async function resolveNativeBinary({ env = process.env, root = packageRoo
|
|
|
75
76
|
if (native.name !== target.package || native.version !== manifest.version) {
|
|
76
77
|
throw new Error(`PRNext native version mismatch: expected ${target.package}@${manifest.version}, found ${native.name}@${native.version}. Reinstall matching versions.`);
|
|
77
78
|
}
|
|
78
|
-
const binary = path.join(path.dirname(nativeManifestFile), 'bin',
|
|
79
|
+
const binary = path.join(path.dirname(nativeManifestFile), 'bin', nativeBinaryName(target.os));
|
|
79
80
|
try { return await executableFile(binary); }
|
|
80
81
|
catch (cause) { throw new Error(`PRNext native executable is missing or not executable in ${target.package}. Reinstall the package.`, { cause }); }
|
|
81
82
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thomas.f/prnext",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11-alpha",
|
|
4
|
+
"license": "MIT",
|
|
4
5
|
"description": "Experimental Rust-powered runtime for Next.js apps, React and npm",
|
|
5
6
|
"homepage": "https://prnext.dev",
|
|
6
7
|
"repository": {
|
|
@@ -116,10 +117,12 @@
|
|
|
116
117
|
"react-server-dom-webpack": "19.3.0"
|
|
117
118
|
},
|
|
118
119
|
"optionalDependencies": {
|
|
119
|
-
"prnext-darwin-arm64": "0.1.
|
|
120
|
-
"prnext-darwin-x64": "0.1.
|
|
121
|
-
"prnext-linux-arm64-gnu": "0.1.
|
|
122
|
-
"prnext-linux-x64-gnu": "0.1.
|
|
120
|
+
"prnext-darwin-arm64": "0.1.11-alpha",
|
|
121
|
+
"prnext-darwin-x64": "0.1.11-alpha",
|
|
122
|
+
"prnext-linux-arm64-gnu": "0.1.11-alpha",
|
|
123
|
+
"prnext-linux-x64-gnu": "0.1.11-alpha",
|
|
124
|
+
"@thomas.f/prnext-win32-arm64-msvc": "0.1.11-alpha",
|
|
125
|
+
"@thomas.f/prnext-win32-x64-msvc": "0.1.11-alpha"
|
|
123
126
|
},
|
|
124
127
|
"engines": {
|
|
125
128
|
"node": ">=22"
|
package/release/verify.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { packageRoot, packageManifest, platforms, sourceCheckout } from '../nati
|
|
|
6
6
|
export async function verifyPackage() {
|
|
7
7
|
const pkg = packageManifest;
|
|
8
8
|
if (pkg.private === true) throw new Error(`${pkg.name} is marked private.`);
|
|
9
|
-
if (!/^\d+\.\d+\.\d+-alpha
|
|
9
|
+
if (!/^\d+\.\d+\.\d+-alpha(?:\.\d+)?$/.test(pkg.version)) throw new Error('This release workflow requires an explicit x.y.z-alpha or x.y.z-alpha.N version.');
|
|
10
10
|
if (pkg.publishConfig?.tag !== 'alpha' || pkg.publishConfig?.access !== 'public') throw new Error('Alpha releases require publishConfig.tag=alpha and access=public.');
|
|
11
11
|
if (pkg.bin?.prn !== './cli.mjs' || pkg.bin?.prnext !== './cli.mjs') throw new Error('Both prn and prnext must point to cli.mjs.');
|
|
12
12
|
const expected = Object.fromEntries(platforms.map(item => [item.package, pkg.version]));
|
|
@@ -35,7 +35,7 @@ export async function verifyPublishedNatives({ registry = 'https://registry.npmj
|
|
|
35
35
|
const problems = [];
|
|
36
36
|
for (const target of platforms) {
|
|
37
37
|
try {
|
|
38
|
-
const url = new URL(`${target.package}/${packageManifest.version}`, registry);
|
|
38
|
+
const url = new URL(`${encodeURIComponent(target.package)}/${encodeURIComponent(packageManifest.version)}`, registry);
|
|
39
39
|
const response = await fetcher(url, { signal: AbortSignal.timeout(15_000) });
|
|
40
40
|
if (!response.ok) throw new Error(`registry returned HTTP ${response.status}`);
|
|
41
41
|
const pkg = await response.json();
|