Repository navigation
perf(cli): single-process dev server with Rust CLI wrapper - #87941
feedthejim wants to merge 2 commits into
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
5aa417d to
991c442
Compare
80e6a7b to
3b77b31
Compare
|
Allow CI Workflow Run
Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer |
1 similar comment
|
Allow CI Workflow Run
Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer |
991c442 to
ff4ff03
Compare
96f6373 to
f196646
Compare
5541176 to
e359b2b
Compare
f196646 to
58e04f1
Compare
58e04f1 to
7afaf27
Compare
e359b2b to
88f6518
Compare
CodSpeed Performance ReportMerging #87941 will not alter performanceComparing Summary
Footnotes
|
| await startServer({ | ||
| ...devServerOptions, | ||
| selfSignedCertificate: certificate, | ||
| }) |
There was a problem hiding this comment.
The dev server no longer supports auto-restart when configuration files change if the Rust CLI wrapper is not available. When startServer() detects a config change, it calls process.exit(77), expecting a Rust wrapper to catch this exit code and restart the process. However, when falling back to the Node.js entry point, this exit code is not handled, causing the dev server to exit permanently instead of restarting.
View Details
📝 Patch Details
diff --git a/packages/next/bin/next b/packages/next/bin/next
index 84eee1314d..816b148735 100755
--- a/packages/next/bin/next
+++ b/packages/next/bin/next
@@ -10,4 +10,7 @@ for pkg in "$DIR"/../node_modules/@next/cli-*/next; do
done
# Fallback: Node.js
+# Set NEXT_CLI_NO_RUST to indicate we're running without the Rust wrapper.
+# This allows the Node.js dev server to implement its own restart loop for config changes.
+export NEXT_CLI_NO_RUST=1
exec node "$DIR/../dist/bin/next" "$@"
diff --git a/packages/next/src/cli/next-dev.ts b/packages/next/src/cli/next-dev.ts
index 16eadfa57c..49838014b7 100644
--- a/packages/next/src/cli/next-dev.ts
+++ b/packages/next/src/cli/next-dev.ts
@@ -228,10 +228,36 @@ const nextDev = async (
process.env.TURBOPACK = '1'
}
+ // Track if we're in a restart loop to handle exit code 77
+ // Only needed when running without the Rust CLI wrapper (NEXT_CLI_NO_RUST)
+ let restartExitCode: number | null = null
+ const originalExit = process.exit
+ const noRustWrapper = process.env.NEXT_CLI_NO_RUST === '1'
+
+ // Intercept process.exit to handle restart signals from the dev server
+ // When startServer() detects a config file change, it calls process.exit(77)
+ // The Rust wrapper catches this and restarts, but when running directly with Node.js,
+ // we need to handle it here to maintain dev server functionality
+ if (noRustWrapper) {
+ process.exit = function (code?: number | string) {
+ const exitCode = typeof code === 'string' ? parseInt(code, 10) : code || 0
+ // Exit code 77 signals "restart needed" (config file changed)
+ if (exitCode === 77) {
+ restartExitCode = 77
+ // Don't actually exit - we'll loop back and restart
+ return undefined
+ }
+ // For all other exit codes, actually exit the process
+ return originalExit.call(process, code as never)
+ } as any
+ }
+
const runDevServer = async (reboot: boolean) => {
try {
// Load startServer from bundled version with optional bytecode caching
- // The Rust wrapper handles restarts via exit code 77
+ // The Rust wrapper handles restarts via exit code 77.
+ // When running without the Rust wrapper, the intercepted process.exit() above
+ // will catch exit code 77 and trigger a restart.
const { startServer } =
require('../server/lib/start-server-with-cache') as typeof import('../server/lib/start-server-with-cache')
@@ -272,7 +298,29 @@ const nextDev = async (
}
}
- await runDevServer(false)
+ // Restart loop for the Node.js fallback path
+ // When NEXT_CLI_NO_RUST=1 (running without the Rust CLI wrapper),
+ // we need to handle config file changes and restart the server.
+ // The Rust wrapper has its own restart loop and won't reach this code.
+ if (noRustWrapper) {
+ let isRestart = false
+ while (true) {
+ restartExitCode = null
+ await runDevServer(isRestart)
+
+ // If startServer exited with code 77, loop back and restart
+ if (restartExitCode === 77) {
+ isRestart = true
+ continue
+ }
+ // Otherwise, exit normally
+ break
+ }
+ } else {
+ // When running with the Rust CLI wrapper, it handles the restart loop.
+ // We just run the server once and exit (potentially with code 77 for restarts).
+ await runDevServer(false)
+ }
}
export { nextDev }
Analysis
Dev server exits permanently when configuration files change in Node.js fallback mode
What fails: When running next dev without the Rust CLI wrapper (@next/cli-* binary), the dev server exits with code 77 and does not restart when configuration files change (next.config.js, next.config.mjs, tsconfig.json, etc.)
How to reproduce:
-
Run Next.js dev server directly through the Node.js fallback path (bypassing the Rust wrapper):
# Simulate the fallback: directly run Node.js instead of using the shell wrapper node packages/next/dist/bin/next dev -
Modify a config file that triggers the watcher (e.g., next.config.js, tsconfig.json)
-
Observe: The dev server logs "Found a change in [filename]. Restarting the server to apply the changes..." then exits with code 77
-
The dev server does not restart - it remains stopped
Expected behavior: The dev server should detect the config file change and restart the server, just like it does when using the Rust wrapper
Root cause: The architecture change in commit 88f651876c moved restart loop logic from JavaScript to a Rust wrapper. The Rust binary correctly handles exit code 77 by looping and restarting the Node.js process. However, when the Rust binary is unavailable and the shell script (packages/next/bin/next) falls back to directly executing node, there is no restart loop in the Node.js code to handle the exit code 77 signal from the config file watcher in start-server.ts.
Affected scenarios:
- Developers on unsupported platforms where Rust binaries aren't available
- Direct invocation of Node.js entry point (bypassing the shell script)
- Failed or missing binary installation (fallback to Node.js)
- Development environments where the Rust wrapper is not available
References:
- Entry point:
packages/next/bin/next(shell script) - Node.js entry point:
packages/next/src/bin/next.ts - Dev CLI:
packages/next/src/cli/next-dev.ts - Config file watcher:
packages/next/src/server/lib/start-server.ts(line 518) - Exit code constant:
packages/next/src/server/lib/utils.ts(RESTART_EXIT_CODE = 77) - Rust wrapper:
crates/next-cli/src/main.rs(handles restart loop)
88f6518 to
ef33eaa
Compare
7afaf27 to
39cecfc
Compare
5b4ce7a to
9bc50a8
Compare
39cecfc to
2258256
Compare
e1e06f4 to
55e1a6e
Compare
22b8f36 to
5550f78
Compare
5550f78 to
fb95fe5
Compare
4daf945 to
b52b486
Compare
fb95fe5 to
060b263
Compare
b52b486 to
9e92ac3
Compare
060b263 to
1f6c875
Compare
9e92ac3 to
d1c86ed
Compare
bbecde9 to
ce856be
Compare
d1c86ed to
763fecf
Compare
763fecf to
28f621b
Compare
ce856be to
6816397
Compare
✅ Tests PassedUpdated 2026-01-02 14:55:58 UTC · Commit: 86ee4c8 |
| } | ||
|
|
||
| const runDevServer = async (reboot: boolean) => { | ||
| try { | ||
| // Load startServer from bundled version with optional bytecode caching |
There was a problem hiding this comment.
The refactored dev server code removes trace upload logic that was previously executed when the server needed to restart due to config file changes. This causes telemetry traces to be lost on restart.
View Details
📝 Patch Details
diff --git a/packages/next/src/cli/next-dev.ts b/packages/next/src/cli/next-dev.ts
index a569440fd7..0928805148 100644
--- a/packages/next/src/cli/next-dev.ts
+++ b/packages/next/src/cli/next-dev.ts
@@ -222,6 +222,25 @@ const nextDev = async (
traceUploadUrl = options.experimentalUploadTrace
}
+ // Handle config change restart (exit code 77) with trace upload
+ // When config files change, start-server.ts will exit with code 77.
+ // We need to upload traces before the process restarts to preserve metrics.
+ const RESTART_EXIT_CODE = 77
+ process.on('exit', (code) => {
+ if (code === RESTART_EXIT_CODE && traceUploadUrl && config) {
+ const uploadTrace = (
+ require('../trace/upload-trace') as typeof import('../trace/upload-trace')
+ ).default
+ uploadTrace({
+ traceUploadUrl,
+ mode: 'dev',
+ projectDir: dir,
+ distDir: config.distDir,
+ isTurboSession: bundler === Bundler.Turbopack,
+ })
+ }
+ })
+
const devServerOptions: StartServerOptions = {
dir,
port,
diff --git a/packages/next/src/next-devtools/server/restart-dev-server-middleware.ts b/packages/next/src/next-devtools/server/restart-dev-server-middleware.ts
index f410e3a4a3..39fb9b7c3d 100644
--- a/packages/next/src/next-devtools/server/restart-dev-server-middleware.ts
+++ b/packages/next/src/next-devtools/server/restart-dev-server-middleware.ts
@@ -1,6 +1,7 @@
import type { ServerResponse, IncomingMessage } from 'http'
import type { Telemetry } from '../../telemetry/storage'
import { RESTART_EXIT_CODE } from '../../server/lib/utils'
+import { flushAllTraces } from '../../trace'
import { middlewareResponse } from './middleware-response'
import type { Project } from '../../build/swc/types'
import { invalidateFileSystemCache as invalidateWebpackFileSystemCache } from '../../build/webpack/cache-invalidation'
@@ -60,6 +61,8 @@ export function getRestartDevServerMiddleware({
// TODO: Use flushDetached
await telemetry.flush()
+ // Flush trace events before restarting to ensure they are persisted
+ await flushAllTraces({ end: false })
// do this async to try to give the response a chance to send
// it's not really important if it doesn't though
diff --git a/packages/next/src/server/lib/start-server.ts b/packages/next/src/server/lib/start-server.ts
index bb7e737cf4..31d2b4cb48 100644
--- a/packages/next/src/server/lib/start-server.ts
+++ b/packages/next/src/server/lib/start-server.ts
@@ -515,6 +515,9 @@ export async function startServer(
filename
)}. Restarting the server to apply the changes...`
)
+ // Flush all trace events before restarting to ensure they are persisted
+ // The parent process will handle uploading traces if needed
+ await flushAllTraces({ end: false })
process.exit(RESTART_EXIT_CODE)
})
}
diff --git a/test-trace b/test-trace
new file mode 100644
index 0000000000..20083a5dd2
--- /dev/null
+++ b/test-trace
@@ -0,0 +1 @@
+{"event":"config-change"}
Analysis
Telemetry traces lost on dev server restart with config file changes
What fails: When Next.js dev server restarts due to config file changes (e.g., next.config.js modification), telemetry trace data is lost even with --experimental-upload-trace flag enabled.
How to reproduce:
- Start Next.js dev server with trace uploads enabled:
next dev --experimental-upload-trace=<url> - Generate some trace events (page visits, builds)
- Modify
next.config.js - Observe server restarts
- Check if traces are uploaded - they will not be because they were never flushed/uploaded before restart
What happens: When config files are detected as changed, packages/next/src/server/lib/start-server.ts (line 518) calls process.exit(77) to signal a restart. This exit happens before:
- Buffered trace events are flushed to disk
- Traces are uploaded via the telemetry service
The new single-process architecture (introduced in commit 09cbf1b85c) removed the parent process's restart handler that previously uploaded traces before restarting.
Expected behavior: Trace events should be flushed and uploaded before the process exits with restart code 77, preserving observability across dev server restarts.
Root cause: The refactoring to single-process dev server with Rust CLI wrapper removed the explicit trace upload logic that was previously in the child process exit handler. Trace events are buffered in memory (batched every 100 events), and without flushing, events < 100 are lost when the process exits.
References:
- Old code (before commit 09cbf1b85c): Had
child.on('exit', async (code, signal) => { if (code === RESTART_EXIT_CODE) { uploadTrace(...); return startServer(...); } } - Trace batching:
packages/next/src/trace/report/to-json.tsline 10-17 - Restart code constant:
packages/next/src/server/lib/utils.tsline 257
| /// Check if user has already set memory limit in NODE_OPTIONS | ||
| fn user_set_memory_limit() -> bool { | ||
| env::var("NODE_OPTIONS") | ||
| .map(|opts| opts.contains("max-old-space-size") || opts.contains("max_old_space_size")) | ||
| .unwrap_or(false) | ||
| } |
There was a problem hiding this comment.
The Rust CLI wrapper doesn't handle conflicts between NODE_OPTIONS inspect flags and command-line --inspect flags. If NODE_OPTIONS contains an inspect flag and the user also passes --inspect on the command line, Node.js will error due to conflicting debug ports.
View Details
📝 Patch Details
diff --git a/crates/next-cli/src/main.rs b/crates/next-cli/src/main.rs
index e00084b6ec..df11bced6c 100644
--- a/crates/next-cli/src/main.rs
+++ b/crates/next-cli/src/main.rs
@@ -84,6 +84,38 @@ fn user_set_memory_limit() -> bool {
.unwrap_or(false)
}
+/// Remove inspect flags from NODE_OPTIONS string
+/// This is extracted to a separate function for testing
+fn filter_node_options_inspect(node_options: &str) -> Option<String> {
+ let cleaned = node_options
+ .split_whitespace()
+ .filter(|arg| {
+ !arg.starts_with("--inspect=") &&
+ !arg.starts_with("--inspect-brk") &&
+ !arg.starts_with("--inspect_brk") &&
+ *arg != "--inspect"
+ })
+ .collect::<Vec<_>>()
+ .join(" ");
+
+ if cleaned.is_empty() {
+ None
+ } else {
+ Some(cleaned)
+ }
+}
+
+/// Remove inspect flags from NODE_OPTIONS to avoid conflicts with command-line flags
+fn remove_inspect_from_node_options() {
+ if let Ok(node_options) = env::var("NODE_OPTIONS") {
+ if let Some(cleaned) = filter_node_options_inspect(&node_options) {
+ env::set_var("NODE_OPTIONS", cleaned);
+ } else {
+ env::remove_var("NODE_OPTIONS");
+ }
+ }
+}
+
/// Get total system memory in MB
fn get_system_memory_mb() -> Option<u64> {
#[cfg(target_os = "linux")]
@@ -128,6 +160,12 @@ fn main() {
let parsed = parse_args(&args);
let script = get_script_path();
+ // If user provided inspect flags on command line, remove any from NODE_OPTIONS
+ // to avoid conflicts (e.g., NODE_OPTIONS='--inspect' and --inspect=9229 both trying to use port 9229)
+ if !parsed.node_flags.is_empty() {
+ remove_inspect_from_node_options();
+ }
+
// Calculate memory limit once (50% of system memory)
let memory_limit_mb = get_system_memory_mb().map(|mb| mb / 2);
let should_set_memory = memory_limit_mb.is_some() && !user_set_memory_limit();
@@ -251,4 +289,46 @@ mod tests {
let content = "SomeOther: 12345 kB\n";
assert_eq!(parse_linux_meminfo(content), None);
}
+
+ #[test]
+ fn test_filter_node_options_inspect_removes_inspect() {
+ assert_eq!(filter_node_options_inspect("--inspect"), None);
+ assert_eq!(filter_node_options_inspect("--inspect=9229"), None);
+ assert_eq!(filter_node_options_inspect("--inspect-brk"), None);
+ assert_eq!(filter_node_options_inspect("--inspect-brk=9229"), None);
+ assert_eq!(filter_node_options_inspect("--inspect_brk"), None);
+ assert_eq!(filter_node_options_inspect("--inspect_brk=9229"), None);
+ }
+
+ #[test]
+ fn test_filter_node_options_inspect_keeps_other_options() {
+ assert_eq!(
+ filter_node_options_inspect("--max-old-space-size=4096"),
+ Some("--max-old-space-size=4096".to_string())
+ );
+ assert_eq!(
+ filter_node_options_inspect("--max-old-space-size=4096 --inspect=9229"),
+ Some("--max-old-space-size=4096".to_string())
+ );
+ assert_eq!(
+ filter_node_options_inspect("--inspect --max-old-space-size=4096"),
+ Some("--max-old-space-size=4096".to_string())
+ );
+ assert_eq!(
+ filter_node_options_inspect("--experimental-foo --inspect-brk=9229 --bar=baz"),
+ Some("--experimental-foo --bar=baz".to_string())
+ );
+ }
+
+ #[test]
+ fn test_filter_node_options_inspect_multiple() {
+ assert_eq!(
+ filter_node_options_inspect("--inspect --inspect-brk"),
+ None
+ );
+ assert_eq!(
+ filter_node_options_inspect("--max-old-space-size=4096 --inspect --inspect-brk"),
+ Some("--max-old-space-size=4096".to_string())
+ );
+ }
}
Analysis
Conflicting Node.js inspect flags not handled in Rust CLI wrapper
What fails: The Rust CLI wrapper (crates/next-cli/src/main.rs) extracts --inspect* flags from command-line arguments and passes them to Node.js without removing conflicting inspect flags from the inherited NODE_OPTIONS environment variable. When NODE_OPTIONS contains --inspect or --inspect-brk and the user also passes --inspect on the command line, Node.js errors with "Starting inspector on 127.0.0.1:9229 failed: address already in use".
How to reproduce:
# Set up developer environment with global debug inspection
export NODE_OPTIONS="--inspect"
# User runs next dev with explicit inspect flag
next dev --inspect=9229Result: Node.js startup fails with error:
Starting inspector on 127.0.0.1:9229 failed: address already in use
Expected: The Rust CLI should detect the command-line --inspect flag and remove conflicting inspect flags from NODE_OPTIONS before spawning Node.js, allowing both flag sources to coexist without conflict. This behavior was present in the JavaScript implementation (commit 4e27b90) which used getNodeOptionsWithoutInspect() to filter NODE_OPTIONS when inspect was provided.
Root cause: The Rust CLI is a separate compiled binary that doesn't use the JavaScript utility functions. It needs its own implementation to handle this conflict, similar to the old JavaScript code: https://github.com/vercel/next.js/commit/4e27b908fe
|
Allow CI Workflow Run
Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer |
| const child = spawn( | ||
| process.execPath, | ||
| [...nodeArgs, ...inspectArgs, scriptPath, ...scriptArgs], | ||
| { stdio: 'inherit', env: process.env } |
There was a problem hiding this comment.
| const child = spawn( | |
| process.execPath, | |
| [...nodeArgs, ...inspectArgs, scriptPath, ...scriptArgs], | |
| { stdio: 'inherit', env: process.env } | |
| // Set up environment with optimizations matching the Rust binary | |
| const env = { ...process.env } | |
| // macOS: limit file watchers to avoid slow cleanup | |
| // https://github.com/nodejs/node/issues/29949 | |
| if (platform === 'darwin' && scriptArgs[0] === 'dev') { | |
| env.WATCHPACK_WATCHER_LIMIT = '20' | |
| } | |
| const child = spawn( | |
| process.execPath, | |
| [...nodeArgs, ...inspectArgs, scriptPath, ...scriptArgs], | |
| { stdio: 'inherit', env } |
The Node.js fallback doesn't set WATCHPACK_WATCHER_LIMIT on macOS, causing the file watcher optimization to be skipped when the Rust binary isn't available.
View Details
Analysis
Missing WATCHPACK_WATCHER_LIMIT optimization in Node.js fallback on macOS
What fails: The runWithNode() fallback in packages/next/bin/next doesn't set the WATCHPACK_WATCHER_LIMIT environment variable on macOS when running the dev command, causing file watcher performance degradation
How to reproduce:
- On macOS, ensure the Rust CLI binary is not available (delete the @next/cli-* package or simulate unavailability)
- Run
next dev - The fallback path in
packages/next/bin/nextis used instead of the Rust binary
Result: The WATCHPACK_WATCHER_LIMIT environment variable is not set, causing slower file watcher cleanup on macOS
Expected: Should match the Rust CLI behavior (crates/next-cli/src/main.rs, lines 158-161) which sets WATCHPACK_WATCHER_LIMIT=20 on macOS when running dev
Context: The watchpack v2.0.0 release introduced a limit on the maximum number of OS watchers (2000 on macOS). The WATCHPACK_WATCHER_LIMIT environment variable allows tuning this limit to optimize performance. The Rust binary correctly applies this optimization on macOS, but the Node.js fallback was missing this logic, causing inconsistent behavior between the two execution paths.
Stats from current PRDefault Build (Increase detected
|
| vercel/next.js canary | vercel/next.js perf-dev-rust-cli | Change | |
|---|---|---|---|
| buildDuration | 18.2s | 15.1s | N/A |
| buildDurationCached | 13.8s | 10.9s | N/A |
| nodeModulesSize | 457 MB | 463 MB | |
| nextStartRea..uration (ms) | 718ms | 750ms | N/A |
Client Bundles (main, webpack) Overall increase ⚠️
| vercel/next.js canary | vercel/next.js perf-dev-rust-cli | Change | |
|---|---|---|---|
| 2086.HASH.js gzip | 169 B | 169 B | ✓ |
| 2161-HASH.js gzip | 5.39 kB | 5.41 kB | N/A |
| 2747-HASH.js gzip | 4.48 kB | 4.46 kB | N/A |
| 4322-HASH.js gzip | 51.2 kB | 51 kB | N/A |
| ec793fe8-HASH.js gzip | 62.3 kB | 62.3 kB | N/A |
| framework-HASH.js gzip | 59.8 kB | 59.8 kB | N/A |
| main-app-HASH.js gzip | 251 B | 254 B | N/A |
| main-HASH.js gzip | 38.4 kB | 38.7 kB | |
| webpack-HASH.js gzip | 1.68 kB | 1.69 kB | N/A |
| Overall change | 38.5 kB | 38.9 kB |
Legacy Client Bundles (polyfills)
| vercel/next.js canary | vercel/next.js perf-dev-rust-cli | Change | |
|---|---|---|---|
| polyfills-HASH.js gzip | 39.4 kB | 39.4 kB | ✓ |
| Overall change | 39.4 kB | 39.4 kB | ✓ |
Client Pages
| vercel/next.js canary | vercel/next.js perf-dev-rust-cli | Change | |
|---|---|---|---|
| _app-HASH.js gzip | 194 B | 193 B | N/A |
| _error-HASH.js gzip | 182 B | 182 B | ✓ |
| css-HASH.js gzip | 336 B | 335 B | N/A |
| dynamic-HASH.js gzip | 1.8 kB | 1.8 kB | N/A |
| edge-ssr-HASH.js gzip | 256 B | 256 B | ✓ |
| head-HASH.js gzip | 352 B | 349 B | N/A |
| hooks-HASH.js gzip | 385 B | 384 B | N/A |
| image-HASH.js gzip | 580 B | 580 B | ✓ |
| index-HASH.js gzip | 259 B | 258 B | N/A |
| link-HASH.js gzip | 2.5 kB | 2.51 kB | N/A |
| routerDirect..HASH.js gzip | 319 B | 317 B | N/A |
| script-HASH.js gzip | 385 B | 387 B | N/A |
| withRouter-HASH.js gzip | 316 B | 315 B | N/A |
| 1afbb74e6ecf..834.css gzip | 106 B | 106 B | ✓ |
| Overall change | 1.12 kB | 1.12 kB | ✓ |
Client Build Manifests
| vercel/next.js canary | vercel/next.js perf-dev-rust-cli | Change | |
|---|---|---|---|
| _buildManifest.js gzip | 738 B | 738 B | ✓ |
| Overall change | 738 B | 738 B | ✓ |
Rendered Page Sizes
| vercel/next.js canary | vercel/next.js perf-dev-rust-cli | Change | |
|---|---|---|---|
| index.html gzip | 523 B | 525 B | N/A |
| link.html gzip | 537 B | 539 B | N/A |
| withRouter.html gzip | 520 B | 521 B | N/A |
| Overall change | 0 B | 0 B | ✓ |
Edge SSR bundle Size Overall increase ⚠️
| vercel/next.js canary | vercel/next.js perf-dev-rust-cli | Change | |
|---|---|---|---|
| edge-ssr.js gzip | 124 kB | 124 kB | N/A |
| page.js gzip | 237 kB | 238 kB | |
| Overall change | 237 kB | 238 kB |
Middleware size Overall increase ⚠️
| vercel/next.js canary | vercel/next.js perf-dev-rust-cli | Change | |
|---|---|---|---|
| middleware-b..fest.js gzip | 652 B | 655 B | N/A |
| middleware-r..fest.js gzip | 155 B | 156 B | N/A |
| middleware.js gzip | 32.8 kB | 32.9 kB | |
| edge-runtime..pack.js gzip | 846 B | 846 B | ✓ |
| Overall change | 33.6 kB | 33.7 kB |
Next Runtimes Overall increase ⚠️
| vercel/next.js canary | vercel/next.js perf-dev-rust-cli | Change | |
|---|---|---|---|
| app-page-exp...dev.js gzip | 301 kB | 302 kB | |
| app-page-exp..prod.js gzip | 156 kB | 156 kB | ✓ |
| app-page-tur...dev.js gzip | 301 kB | 301 kB | |
| app-page-tur..prod.js gzip | 156 kB | 156 kB | ✓ |
| app-page-tur...dev.js gzip | 298 kB | 298 kB | |
| app-page-tur..prod.js gzip | 154 kB | 154 kB | ✓ |
| app-page.run...dev.js gzip | 298 kB | 298 kB | |
| app-page.run..prod.js gzip | 154 kB | 154 kB | ✓ |
| app-route-ex...dev.js gzip | 68.7 kB | 68.7 kB | ✓ |
| app-route-ex..prod.js gzip | 47.5 kB | 47.5 kB | ✓ |
| app-route-tu...dev.js gzip | 68.7 kB | 68.7 kB | ✓ |
| app-route-tu..prod.js gzip | 47.5 kB | 47.5 kB | ✓ |
| app-route-tu...dev.js gzip | 68.3 kB | 68.3 kB | ✓ |
| app-route-tu..prod.js gzip | 47.3 kB | 47.3 kB | ✓ |
| app-route.ru...dev.js gzip | 68.3 kB | 68.3 kB | ✓ |
| app-route.ru..prod.js gzip | 47.3 kB | 47.3 kB | ✓ |
| dist_client_...dev.js gzip | 324 B | 324 B | ✓ |
| dist_client_...dev.js gzip | 326 B | 326 B | ✓ |
| dist_client_...dev.js gzip | 318 B | 318 B | ✓ |
| dist_client_...dev.js gzip | 317 B | 317 B | ✓ |
| pages-api-tu...dev.js gzip | 41.1 kB | 41.1 kB | ✓ |
| pages-api-tu..prod.js gzip | 31.2 kB | 31.2 kB | ✓ |
| pages-api.ru...dev.js gzip | 41 kB | 41 kB | ✓ |
| pages-api.ru..prod.js gzip | 31.2 kB | 31.2 kB | ✓ |
| pages-turbo....dev.js gzip | 50.7 kB | 50.7 kB | ✓ |
| pages-turbo...prod.js gzip | 38.2 kB | 38.2 kB | ✓ |
| pages.runtim...dev.js gzip | 50.6 kB | 50.6 kB | ✓ |
| pages.runtim..prod.js gzip | 38.1 kB | 38.1 kB | ✓ |
| server.runti..prod.js gzip | 59.9 kB | 60.3 kB | |
| Overall change | 2.67 MB | 2.67 MB |
build cache Overall increase ⚠️
| vercel/next.js canary | vercel/next.js perf-dev-rust-cli | Change | |
|---|---|---|---|
| 0.pack gzip | 3.61 MB | 3.61 MB | |
| index.pack gzip | 98.1 kB | 97.8 kB | N/A |
| Overall change | 3.61 MB | 3.61 MB |
Diff details
Diff for page.js
Diff too large to display
Diff for middleware.js
Diff too large to display
Diff for edge-ssr.js
Diff too large to display
Diff for _buildManifest.js
@@ -611,35 +611,35 @@ self.__BUILD_MANIFEST = (function (a, b, c) {
numHashes: NaN,
bitArray: [],
},
- "/": ["static\u002Fchunks\u002Fpages\u002Findex-dda7d8b64d4ba15c.js"],
+ "/": ["static\u002Fchunks\u002Fpages\u002Findex-d95f7ec6af4d2644.js"],
"/_error": [
- "static\u002Fchunks\u002Fpages\u002F_error-6ef44d3954f25711.js",
+ "static\u002Fchunks\u002Fpages\u002F_error-209c0c82205a7c9f.js",
],
"/css": [
"static\u002Fcss\u002Fded6b86ab9cc0a1f.css",
- "static\u002Fchunks\u002Fpages\u002Fcss-c8aaa7211416a045.js",
+ "static\u002Fchunks\u002Fpages\u002Fcss-62710339bc830ded.js",
],
"/dynamic": [
- "static\u002Fchunks\u002Fpages\u002Fdynamic-d53bb7f318f342c2.js",
+ "static\u002Fchunks\u002Fpages\u002Fdynamic-8ed3486bb68dd6c6.js",
],
"/edge-ssr": [
- "static\u002Fchunks\u002Fpages\u002Fedge-ssr-1383106d4a3e7d72.js",
+ "static\u002Fchunks\u002Fpages\u002Fedge-ssr-0db9f7bb610d3072.js",
],
- "/head": ["static\u002Fchunks\u002Fpages\u002Fhead-1db1c4be1a45662f.js"],
- "/hooks": ["static\u002Fchunks\u002Fpages\u002Fhooks-e6deee5b72a5b112.js"],
+ "/head": ["static\u002Fchunks\u002Fpages\u002Fhead-747416c4075e42aa.js"],
+ "/hooks": ["static\u002Fchunks\u002Fpages\u002Fhooks-8b10205be7505244.js"],
"/image": [
- "static\u002Fchunks\u002F2747-90c828280005c0c3.js",
- "static\u002Fchunks\u002Fpages\u002Fimage-868472cd5e84efc9.js",
+ "static\u002Fchunks\u002F6349-ee9aecde860d4832.js",
+ "static\u002Fchunks\u002Fpages\u002Fimage-f90ae17c3ad1d38b.js",
],
- "/link": ["static\u002Fchunks\u002Fpages\u002Flink-0ec374e48b2ce5d9.js"],
+ "/link": ["static\u002Fchunks\u002Fpages\u002Flink-6ab9a67b348df1bf.js"],
"/routerDirect": [
- "static\u002Fchunks\u002Fpages\u002FrouterDirect-1a34bfadbc088491.js",
+ "static\u002Fchunks\u002Fpages\u002FrouterDirect-f132fb471e65a8b9.js",
],
"/script": [
- "static\u002Fchunks\u002Fpages\u002Fscript-52320f59afbd096d.js",
+ "static\u002Fchunks\u002Fpages\u002Fscript-8e2028e44b8da2d9.js",
],
"/withRouter": [
- "static\u002Fchunks\u002Fpages\u002FwithRouter-3b1a16d3dfa21c16.js",
+ "static\u002Fchunks\u002Fpages\u002FwithRouter-a86d259faa012c1c.js",
],
sortedPages: [
"\u002F",Diff for dynamic-HASH.js
@@ -1,17 +1,24 @@
(self["webpackChunk_N_E"] = self["webpackChunk_N_E"] || []).push([
[2291],
{
- /***/ 1231: /***/ (
- module,
+ /***/ 2604: /***/ (
+ __unused_webpack_module,
__unused_webpack_exports,
__webpack_require__
) => {
- module.exports = __webpack_require__(4464);
+ (window.__NEXT_P = window.__NEXT_P || []).push([
+ "/dynamic",
+ function () {
+ return __webpack_require__(3643);
+ },
+ ]);
+ if (false) {
+ }
/***/
},
- /***/ 2101: /***/ (
+ /***/ 3401: /***/ (
__unused_webpack_module,
exports,
__webpack_require__
@@ -38,24 +45,63 @@
/***/
},
- /***/ 3458: /***/ (
+ /***/ 3643: /***/ (
__unused_webpack_module,
- __unused_webpack_exports,
+ __webpack_exports__,
__webpack_require__
) => {
- (window.__NEXT_P = window.__NEXT_P || []).push([
- "/dynamic",
- function () {
- return __webpack_require__(9381);
- },
- ]);
- if (false) {
- }
+ "use strict";
+ __webpack_require__.r(__webpack_exports__);
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
+ /* harmony export */ __N_SSP: () => /* binding */ __N_SSP,
+ /* harmony export */ default: () => __WEBPACK_DEFAULT_EXPORT__,
+ /* harmony export */
+ });
+ /* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__ =
+ __webpack_require__(3108);
+ /* harmony import */ var next_dynamic__WEBPACK_IMPORTED_MODULE_1__ =
+ __webpack_require__(3973);
+ /* harmony import */ var next_dynamic__WEBPACK_IMPORTED_MODULE_1___default =
+ /*#__PURE__*/ __webpack_require__.n(
+ next_dynamic__WEBPACK_IMPORTED_MODULE_1__
+ );
+
+ const DynamicHello = next_dynamic__WEBPACK_IMPORTED_MODULE_1___default()(
+ () =>
+ __webpack_require__
+ .e(/* import() */ 1596)
+ .then(__webpack_require__.bind(__webpack_require__, 1596))
+ .then((mod) => mod.Hello),
+ {
+ loadableGenerated: {
+ webpack: () => [/*require.resolve*/ 1596],
+ },
+ }
+ );
+ const Page = () =>
+ /*#__PURE__*/ (0, react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)(
+ react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.Fragment,
+ {
+ children: [
+ /*#__PURE__*/ (0,
+ react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("p", {
+ children: "testing next/dynamic size",
+ }),
+ /*#__PURE__*/ (0,
+ react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(
+ DynamicHello,
+ {}
+ ),
+ ],
+ }
+ );
+ var __N_SSP = true;
+ /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = Page;
/***/
},
- /***/ 4464: /***/ (module, exports, __webpack_require__) => {
+ /***/ 3660: /***/ (module, exports, __webpack_require__) => {
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -88,7 +134,7 @@
__webpack_require__(4312)
);
const _loadablesharedruntime = /*#__PURE__*/ _interop_require_default._(
- __webpack_require__(9799)
+ __webpack_require__(4539)
);
const isServerSide = "object" === "undefined";
// Normalize loader to return the module as form { default: Component } for `React.lazy`.
@@ -188,63 +234,17 @@
/***/
},
- /***/ 9381: /***/ (
- __unused_webpack_module,
- __webpack_exports__,
+ /***/ 3973: /***/ (
+ module,
+ __unused_webpack_exports,
__webpack_require__
) => {
- "use strict";
- __webpack_require__.r(__webpack_exports__);
- /* harmony export */ __webpack_require__.d(__webpack_exports__, {
- /* harmony export */ __N_SSP: () => /* binding */ __N_SSP,
- /* harmony export */ default: () => __WEBPACK_DEFAULT_EXPORT__,
- /* harmony export */
- });
- /* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__ =
- __webpack_require__(3108);
- /* harmony import */ var next_dynamic__WEBPACK_IMPORTED_MODULE_1__ =
- __webpack_require__(1231);
- /* harmony import */ var next_dynamic__WEBPACK_IMPORTED_MODULE_1___default =
- /*#__PURE__*/ __webpack_require__.n(
- next_dynamic__WEBPACK_IMPORTED_MODULE_1__
- );
-
- const DynamicHello = next_dynamic__WEBPACK_IMPORTED_MODULE_1___default()(
- () =>
- __webpack_require__
- .e(/* import() */ 2086)
- .then(__webpack_require__.bind(__webpack_require__, 2086))
- .then((mod) => mod.Hello),
- {
- loadableGenerated: {
- webpack: () => [/*require.resolve*/ 2086],
- },
- }
- );
- const Page = () =>
- /*#__PURE__*/ (0, react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsxs)(
- react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.Fragment,
- {
- children: [
- /*#__PURE__*/ (0,
- react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)("p", {
- children: "testing next/dynamic size",
- }),
- /*#__PURE__*/ (0,
- react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(
- DynamicHello,
- {}
- ),
- ],
- }
- );
- var __N_SSP = true;
- /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = Page;
+ module.exports = __webpack_require__(3660);
/***/
},
- /***/ 9799: /***/ (
+ /***/ 4539: /***/ (
__unused_webpack_module,
exports,
__webpack_require__
@@ -286,7 +286,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
const _react = /*#__PURE__*/ _interop_require_default._(
__webpack_require__(4312)
);
- const _loadablecontextsharedruntime = __webpack_require__(2101);
+ const _loadablecontextsharedruntime = __webpack_require__(3401);
function resolve(obj) {
return obj && obj.default ? obj.default : obj;
}
@@ -524,7 +524,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
/******/ var __webpack_exec__ = (moduleId) =>
__webpack_require__((__webpack_require__.s = moduleId));
/******/ __webpack_require__.O(0, [636, 6593, 8792], () =>
- __webpack_exec__(3458)
+ __webpack_exec__(2604)
);
/******/ var __webpack_exports__ = __webpack_require__.O();
/******/ _N_E = __webpack_exports__;Diff for edge-ssr-HASH.js
@@ -1,7 +1,24 @@
(self["webpackChunk_N_E"] = self["webpackChunk_N_E"] || []).push([
[676],
{
- /***/ 1564: /***/ (
+ /***/ 7046: /***/ (
+ __unused_webpack_module,
+ __unused_webpack_exports,
+ __webpack_require__
+ ) => {
+ (window.__NEXT_P = window.__NEXT_P || []).push([
+ "/edge-ssr",
+ function () {
+ return __webpack_require__(7142);
+ },
+ ]);
+ if (false) {
+ }
+
+ /***/
+ },
+
+ /***/ 7142: /***/ (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
@@ -20,30 +37,13 @@
/***/
},
-
- /***/ 4300: /***/ (
- __unused_webpack_module,
- __unused_webpack_exports,
- __webpack_require__
- ) => {
- (window.__NEXT_P = window.__NEXT_P || []).push([
- "/edge-ssr",
- function () {
- return __webpack_require__(1564);
- },
- ]);
- if (false) {
- }
-
- /***/
- },
},
/******/ (__webpack_require__) => {
// webpackRuntimeModules
/******/ var __webpack_exec__ = (moduleId) =>
__webpack_require__((__webpack_require__.s = moduleId));
/******/ __webpack_require__.O(0, [636, 6593, 8792], () =>
- __webpack_exec__(4300)
+ __webpack_exec__(7046)
);
/******/ var __webpack_exports__ = __webpack_require__.O();
/******/ _N_E = __webpack_exports__;Diff for head-HASH.js
@@ -1,34 +1,17 @@
(self["webpackChunk_N_E"] = self["webpackChunk_N_E"] || []).push([
[5350],
{
- /***/ 1548: /***/ (
- __unused_webpack_module,
- __unused_webpack_exports,
- __webpack_require__
- ) => {
- (window.__NEXT_P = window.__NEXT_P || []).push([
- "/head",
- function () {
- return __webpack_require__(4282);
- },
- ]);
- if (false) {
- }
-
- /***/
- },
-
- /***/ 3952: /***/ (
+ /***/ 3770: /***/ (
module,
__unused_webpack_exports,
__webpack_require__
) => {
- module.exports = __webpack_require__(1653);
+ module.exports = __webpack_require__(9025);
/***/
},
- /***/ 4282: /***/ (
+ /***/ 6236: /***/ (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
@@ -43,7 +26,7 @@
/* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__(3108);
/* harmony import */ var next_head__WEBPACK_IMPORTED_MODULE_1__ =
- __webpack_require__(3952);
+ __webpack_require__(3770);
/* harmony import */ var next_head__WEBPACK_IMPORTED_MODULE_1___default =
/*#__PURE__*/ __webpack_require__.n(
next_head__WEBPACK_IMPORTED_MODULE_1__
@@ -76,13 +59,30 @@
/***/
},
+
+ /***/ 6510: /***/ (
+ __unused_webpack_module,
+ __unused_webpack_exports,
+ __webpack_require__
+ ) => {
+ (window.__NEXT_P = window.__NEXT_P || []).push([
+ "/head",
+ function () {
+ return __webpack_require__(6236);
+ },
+ ]);
+ if (false) {
+ }
+
+ /***/
+ },
},
/******/ (__webpack_require__) => {
// webpackRuntimeModules
/******/ var __webpack_exec__ = (moduleId) =>
__webpack_require__((__webpack_require__.s = moduleId));
/******/ __webpack_require__.O(0, [636, 6593, 8792], () =>
- __webpack_exec__(1548)
+ __webpack_exec__(6510)
);
/******/ var __webpack_exports__ = __webpack_require__.O();
/******/ _N_E = __webpack_exports__;Diff for index-HASH.js
@@ -1,24 +1,7 @@
(self["webpackChunk_N_E"] = self["webpackChunk_N_E"] || []).push([
[3332],
{
- /***/ 6376: /***/ (
- __unused_webpack_module,
- __unused_webpack_exports,
- __webpack_require__
- ) => {
- (window.__NEXT_P = window.__NEXT_P || []).push([
- "/",
- function () {
- return __webpack_require__(8460);
- },
- ]);
- if (false) {
- }
-
- /***/
- },
-
- /***/ 8460: /***/ (
+ /***/ 830: /***/ (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
@@ -36,13 +19,30 @@
/***/
},
+
+ /***/ 1938: /***/ (
+ __unused_webpack_module,
+ __unused_webpack_exports,
+ __webpack_require__
+ ) => {
+ (window.__NEXT_P = window.__NEXT_P || []).push([
+ "/",
+ function () {
+ return __webpack_require__(830);
+ },
+ ]);
+ if (false) {
+ }
+
+ /***/
+ },
},
/******/ (__webpack_require__) => {
// webpackRuntimeModules
/******/ var __webpack_exec__ = (moduleId) =>
__webpack_require__((__webpack_require__.s = moduleId));
/******/ __webpack_require__.O(0, [636, 6593, 8792], () =>
- __webpack_exec__(6376)
+ __webpack_exec__(1938)
);
/******/ var __webpack_exports__ = __webpack_require__.O();
/******/ _N_E = __webpack_exports__;Diff for link-HASH.js
@@ -1,7 +1,220 @@
(self["webpackChunk_N_E"] = self["webpackChunk_N_E"] || []).push([
[4672],
{
- /***/ 857: /***/ (module, exports, __webpack_require__) => {
+ /***/ 2774: /***/ (module, exports, __webpack_require__) => {
+ "use strict";
+
+ Object.defineProperty(exports, "__esModule", {
+ value: true,
+ });
+ Object.defineProperty(exports, "useIntersection", {
+ enumerable: true,
+ get: function () {
+ return useIntersection;
+ },
+ });
+ const _react = __webpack_require__(4312);
+ const _requestidlecallback = __webpack_require__(9413);
+ const hasIntersectionObserver =
+ typeof IntersectionObserver === "function";
+ const observers = new Map();
+ const idList = [];
+ function createObserver(options) {
+ const id = {
+ root: options.root || null,
+ margin: options.rootMargin || "",
+ };
+ const existing = idList.find(
+ (obj) => obj.root === id.root && obj.margin === id.margin
+ );
+ let instance;
+ if (existing) {
+ instance = observers.get(existing);
+ if (instance) {
+ return instance;
+ }
+ }
+ const elements = new Map();
+ const observer = new IntersectionObserver((entries) => {
+ entries.forEach((entry) => {
+ const callback = elements.get(entry.target);
+ const isVisible =
+ entry.isIntersecting || entry.intersectionRatio > 0;
+ if (callback && isVisible) {
+ callback(isVisible);
+ }
+ });
+ }, options);
+ instance = {
+ id,
+ observer,
+ elements,
+ };
+ idList.push(id);
+ observers.set(id, instance);
+ return instance;
+ }
+ function observe(element, callback, options) {
+ const { id, observer, elements } = createObserver(options);
+ elements.set(element, callback);
+ observer.observe(element);
+ return function unobserve() {
+ elements.delete(element);
+ observer.unobserve(element);
+ // Destroy observer when there's nothing left to watch:
+ if (elements.size === 0) {
+ observer.disconnect();
+ observers.delete(id);
+ const index = idList.findIndex(
+ (obj) => obj.root === id.root && obj.margin === id.margin
+ );
+ if (index > -1) {
+ idList.splice(index, 1);
+ }
+ }
+ };
+ }
+ function useIntersection({ rootRef, rootMargin, disabled }) {
+ const isDisabled = disabled || !hasIntersectionObserver;
+ const [visible, setVisible] = (0, _react.useState)(false);
+ const elementRef = (0, _react.useRef)(null);
+ const setElement = (0, _react.useCallback)((element) => {
+ elementRef.current = element;
+ }, []);
+ (0, _react.useEffect)(() => {
+ if (hasIntersectionObserver) {
+ if (isDisabled || visible) return;
+ const element = elementRef.current;
+ if (element && element.tagName) {
+ const unobserve = observe(
+ element,
+ (isVisible) => isVisible && setVisible(isVisible),
+ {
+ root: rootRef?.current,
+ rootMargin,
+ }
+ );
+ return unobserve;
+ }
+ } else {
+ if (!visible) {
+ const idleCallback = (0,
+ _requestidlecallback.requestIdleCallback)(() => setVisible(true));
+ return () =>
+ (0, _requestidlecallback.cancelIdleCallback)(idleCallback);
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isDisabled, rootMargin, rootRef, visible, elementRef.current]);
+ const resetVisible = (0, _react.useCallback)(() => {
+ setVisible(false);
+ }, []);
+ return [setElement, visible, resetVisible];
+ }
+ if (
+ (typeof exports.default === "function" ||
+ (typeof exports.default === "object" && exports.default !== null)) &&
+ typeof exports.default.__esModule === "undefined"
+ ) {
+ Object.defineProperty(exports.default, "__esModule", {
+ value: true,
+ });
+ Object.assign(exports.default, exports);
+ module.exports = exports.default;
+ } //# sourceMappingURL=use-intersection.js.map
+
+ /***/
+ },
+
+ /***/ 3351: /***/ (module, exports, __webpack_require__) => {
+ "use strict";
+
+ Object.defineProperty(exports, "__esModule", {
+ value: true,
+ });
+ Object.defineProperty(exports, "useMergedRef", {
+ enumerable: true,
+ get: function () {
+ return useMergedRef;
+ },
+ });
+ const _react = __webpack_require__(4312);
+ function useMergedRef(refA, refB) {
+ const cleanupA = (0, _react.useRef)(null);
+ const cleanupB = (0, _react.useRef)(null);
+ // NOTE: In theory, we could skip the wrapping if only one of the refs is non-null.
+ // (this happens often if the user doesn't pass a ref to Link/Form/Image)
+ // But this can cause us to leak a cleanup-ref into user code (previously via `<Link legacyBehavior>`),
+ // and the user might pass that ref into ref-merging library that doesn't support cleanup refs
+ // (because it hasn't been updated for React 19)
+ // which can then cause things to blow up, because a cleanup-returning ref gets called with `null`.
+ // So in practice, it's safer to be defensive and always wrap the ref, even on React 19.
+ return (0, _react.useCallback)(
+ (current) => {
+ if (current === null) {
+ const cleanupFnA = cleanupA.current;
+ if (cleanupFnA) {
+ cleanupA.current = null;
+ cleanupFnA();
+ }
+ const cleanupFnB = cleanupB.current;
+ if (cleanupFnB) {
+ cleanupB.current = null;
+ cleanupFnB();
+ }
+ } else {
+ if (refA) {
+ cleanupA.current = applyRef(refA, current);
+ }
+ if (refB) {
+ cleanupB.current = applyRef(refB, current);
+ }
+ }
+ },
+ [refA, refB]
+ );
+ }
+ function applyRef(refA, current) {
+ if (typeof refA === "function") {
+ const cleanup = refA(current);
+ if (typeof cleanup === "function") {
+ return cleanup;
+ } else {
+ return () => refA(null);
+ }
+ } else {
+ refA.current = current;
+ return () => {
+ refA.current = null;
+ };
+ }
+ }
+ if (
+ (typeof exports.default === "function" ||
+ (typeof exports.default === "object" && exports.default !== null)) &&
+ typeof exports.default.__esModule === "undefined"
+ ) {
+ Object.defineProperty(exports.default, "__esModule", {
+ value: true,
+ });
+ Object.assign(exports.default, exports);
+ module.exports = exports.default;
+ } //# sourceMappingURL=use-merged-ref.js.map
+
+ /***/
+ },
+
+ /***/ 3440: /***/ (
+ module,
+ __unused_webpack_exports,
+ __webpack_require__
+ ) => {
+ module.exports = __webpack_require__(4757);
+
+ /***/
+ },
+
+ /***/ 4757: /***/ (module, exports, __webpack_require__) => {
"use strict";
/* __next_internal_client_entry_do_not_use__ cjs */
Object.defineProperty(exports, "__esModule", {
@@ -23,22 +236,22 @@
return useLinkStatus;
},
});
- const _interop_require_wildcard = __webpack_require__(8781);
+ const _interop_require_wildcard = __webpack_require__(1162);
const _jsxruntime = __webpack_require__(3108);
const _react = /*#__PURE__*/ _interop_require_wildcard._(
__webpack_require__(4312)
);
- const _resolvehref = __webpack_require__(4055);
- const _islocalurl = __webpack_require__(7175);
- const _formaturl = __webpack_require__(9674);
- const _utils = __webpack_require__(7424);
- const _addlocale = __webpack_require__(589);
- const _routercontextsharedruntime = __webpack_require__(7010);
- const _useintersection = __webpack_require__(2330);
- const _getdomainlocale = __webpack_require__(7207);
- const _addbasepath = __webpack_require__(9942);
- const _usemergedref = __webpack_require__(8067);
- const _erroronce = __webpack_require__(1945);
+ const _resolvehref = __webpack_require__(4691);
+ const _islocalurl = __webpack_require__(4763);
+ const _formaturl = __webpack_require__(3246);
+ const _utils = __webpack_require__(7372);
+ const _addlocale = __webpack_require__(1809);
+ const _routercontextsharedruntime = __webpack_require__(2254);
+ const _useintersection = __webpack_require__(2774);
+ const _getdomainlocale = __webpack_require__(8803);
+ const _addbasepath = __webpack_require__(5834);
+ const _usemergedref = __webpack_require__(3351);
+ const _erroronce = __webpack_require__(6021);
const prefetched = new Set();
function prefetch(router, href, as, options) {
if (false) {
@@ -417,168 +630,43 @@
/***/
},
- /***/ 1945: /***/ (__unused_webpack_module, exports) => {
- "use strict";
-
- Object.defineProperty(exports, "__esModule", {
- value: true,
- });
- Object.defineProperty(exports, "errorOnce", {
- enumerable: true,
- get: function () {
- return errorOnce;
+ /***/ 5326: /***/ (
+ __unused_webpack_module,
+ __unused_webpack_exports,
+ __webpack_require__
+ ) => {
+ (window.__NEXT_P = window.__NEXT_P || []).push([
+ "/link",
+ function () {
+ return __webpack_require__(9058);
},
- });
- let errorOnce = (_) => {};
+ ]);
if (false) {
- } //# sourceMappingURL=error-once.js.map
+ }
/***/
},
- /***/ 2330: /***/ (module, exports, __webpack_require__) => {
+ /***/ 6021: /***/ (__unused_webpack_module, exports) => {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
- Object.defineProperty(exports, "useIntersection", {
+ Object.defineProperty(exports, "errorOnce", {
enumerable: true,
get: function () {
- return useIntersection;
+ return errorOnce;
},
});
- const _react = __webpack_require__(4312);
- const _requestidlecallback = __webpack_require__(601);
- const hasIntersectionObserver =
- typeof IntersectionObserver === "function";
- const observers = new Map();
- const idList = [];
- function createObserver(options) {
- const id = {
- root: options.root || null,
- margin: options.rootMargin || "",
- };
- const existing = idList.find(
- (obj) => obj.root === id.root && obj.margin === id.margin
- );
- let instance;
- if (existing) {
- instance = observers.get(existing);
- if (instance) {
- return instance;
- }
- }
- const elements = new Map();
- const observer = new IntersectionObserver((entries) => {
- entries.forEach((entry) => {
- const callback = elements.get(entry.target);
- const isVisible =
- entry.isIntersecting || entry.intersectionRatio > 0;
- if (callback && isVisible) {
- callback(isVisible);
- }
- });
- }, options);
- instance = {
- id,
- observer,
- elements,
- };
- idList.push(id);
- observers.set(id, instance);
- return instance;
- }
- function observe(element, callback, options) {
- const { id, observer, elements } = createObserver(options);
- elements.set(element, callback);
- observer.observe(element);
- return function unobserve() {
- elements.delete(element);
- observer.unobserve(element);
- // Destroy observer when there's nothing left to watch:
- if (elements.size === 0) {
- observer.disconnect();
- observers.delete(id);
- const index = idList.findIndex(
- (obj) => obj.root === id.root && obj.margin === id.margin
- );
- if (index > -1) {
- idList.splice(index, 1);
- }
- }
- };
- }
- function useIntersection({ rootRef, rootMargin, disabled }) {
- const isDisabled = disabled || !hasIntersectionObserver;
- const [visible, setVisible] = (0, _react.useState)(false);
- const elementRef = (0, _react.useRef)(null);
- const setElement = (0, _react.useCallback)((element) => {
- elementRef.current = element;
- }, []);
- (0, _react.useEffect)(() => {
- if (hasIntersectionObserver) {
- if (isDisabled || visible) return;
- const element = elementRef.current;
- if (element && element.tagName) {
- const unobserve = observe(
- element,
- (isVisible) => isVisible && setVisible(isVisible),
- {
- root: rootRef?.current,
- rootMargin,
- }
- );
- return unobserve;
- }
- } else {
- if (!visible) {
- const idleCallback = (0,
- _requestidlecallback.requestIdleCallback)(() => setVisible(true));
- return () =>
- (0, _requestidlecallback.cancelIdleCallback)(idleCallback);
- }
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [isDisabled, rootMargin, rootRef, visible, elementRef.current]);
- const resetVisible = (0, _react.useCallback)(() => {
- setVisible(false);
- }, []);
- return [setElement, visible, resetVisible];
- }
- if (
- (typeof exports.default === "function" ||
- (typeof exports.default === "object" && exports.default !== null)) &&
- typeof exports.default.__esModule === "undefined"
- ) {
- Object.defineProperty(exports.default, "__esModule", {
- value: true,
- });
- Object.assign(exports.default, exports);
- module.exports = exports.default;
- } //# sourceMappingURL=use-intersection.js.map
-
- /***/
- },
-
- /***/ 4972: /***/ (
- __unused_webpack_module,
- __unused_webpack_exports,
- __webpack_require__
- ) => {
- (window.__NEXT_P = window.__NEXT_P || []).push([
- "/link",
- function () {
- return __webpack_require__(7600);
- },
- ]);
+ let errorOnce = (_) => {};
if (false) {
- }
+ } //# sourceMappingURL=error-once.js.map
/***/
},
- /***/ 7207: /***/ (module, exports, __webpack_require__) => {
+ /***/ 8803: /***/ (module, exports, __webpack_require__) => {
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -590,7 +678,7 @@
return getDomainLocale;
},
});
- const _normalizetrailingslash = __webpack_require__(151);
+ const _normalizetrailingslash = __webpack_require__(1315);
const basePath =
/* unused pure expression or super */ null && (false || "");
function getDomainLocale(path, locale, locales, domainLocales) {
@@ -614,7 +702,7 @@
/***/
},
- /***/ 7600: /***/ (
+ /***/ 9058: /***/ (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
@@ -629,7 +717,7 @@
/* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__(3108);
/* harmony import */ var next_link__WEBPACK_IMPORTED_MODULE_1__ =
- __webpack_require__(7830);
+ __webpack_require__(3440);
/* harmony import */ var next_link__WEBPACK_IMPORTED_MODULE_1___default =
/*#__PURE__*/ __webpack_require__.n(
next_link__WEBPACK_IMPORTED_MODULE_1__
@@ -659,101 +747,13 @@
/***/
},
-
- /***/ 7830: /***/ (
- module,
- __unused_webpack_exports,
- __webpack_require__
- ) => {
- module.exports = __webpack_require__(857);
-
- /***/
- },
-
- /***/ 8067: /***/ (module, exports, __webpack_require__) => {
- "use strict";
-
- Object.defineProperty(exports, "__esModule", {
- value: true,
- });
- Object.defineProperty(exports, "useMergedRef", {
- enumerable: true,
- get: function () {
- return useMergedRef;
- },
- });
- const _react = __webpack_require__(4312);
- function useMergedRef(refA, refB) {
- const cleanupA = (0, _react.useRef)(null);
- const cleanupB = (0, _react.useRef)(null);
- // NOTE: In theory, we could skip the wrapping if only one of the refs is non-null.
- // (this happens often if the user doesn't pass a ref to Link/Form/Image)
- // But this can cause us to leak a cleanup-ref into user code (previously via `<Link legacyBehavior>`),
- // and the user might pass that ref into ref-merging library that doesn't support cleanup refs
- // (because it hasn't been updated for React 19)
- // which can then cause things to blow up, because a cleanup-returning ref gets called with `null`.
- // So in practice, it's safer to be defensive and always wrap the ref, even on React 19.
- return (0, _react.useCallback)(
- (current) => {
- if (current === null) {
- const cleanupFnA = cleanupA.current;
- if (cleanupFnA) {
- cleanupA.current = null;
- cleanupFnA();
- }
- const cleanupFnB = cleanupB.current;
- if (cleanupFnB) {
- cleanupB.current = null;
- cleanupFnB();
- }
- } else {
- if (refA) {
- cleanupA.current = applyRef(refA, current);
- }
- if (refB) {
- cleanupB.current = applyRef(refB, current);
- }
- }
- },
- [refA, refB]
- );
- }
- function applyRef(refA, current) {
- if (typeof refA === "function") {
- const cleanup = refA(current);
- if (typeof cleanup === "function") {
- return cleanup;
- } else {
- return () => refA(null);
- }
- } else {
- refA.current = current;
- return () => {
- refA.current = null;
- };
- }
- }
- if (
- (typeof exports.default === "function" ||
- (typeof exports.default === "object" && exports.default !== null)) &&
- typeof exports.default.__esModule === "undefined"
- ) {
- Object.defineProperty(exports.default, "__esModule", {
- value: true,
- });
- Object.assign(exports.default, exports);
- module.exports = exports.default;
- } //# sourceMappingURL=use-merged-ref.js.map
-
- /***/
- },
},
/******/ (__webpack_require__) => {
// webpackRuntimeModules
/******/ var __webpack_exec__ = (moduleId) =>
__webpack_require__((__webpack_require__.s = moduleId));
/******/ __webpack_require__.O(0, [636, 6593, 8792], () =>
- __webpack_exec__(4972)
+ __webpack_exec__(5326)
);
/******/ var __webpack_exports__ = __webpack_require__.O();
/******/ _N_E = __webpack_exports__;Diff for routerDirect-HASH.js
@@ -1,17 +1,7 @@
(self["webpackChunk_N_E"] = self["webpackChunk_N_E"] || []).push([
[188],
{
- /***/ 1179: /***/ (
- module,
- __unused_webpack_exports,
- __webpack_require__
- ) => {
- module.exports = __webpack_require__(9864);
-
- /***/
- },
-
- /***/ 1640: /***/ (
+ /***/ 2014: /***/ (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
@@ -26,7 +16,7 @@
/* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__(3108);
/* harmony import */ var next_router__WEBPACK_IMPORTED_MODULE_1__ =
- __webpack_require__(1179);
+ __webpack_require__(4169);
/* harmony import */ var next_router__WEBPACK_IMPORTED_MODULE_1___default =
/*#__PURE__*/ __webpack_require__.n(
next_router__WEBPACK_IMPORTED_MODULE_1__
@@ -46,7 +36,17 @@
/***/
},
- /***/ 2172: /***/ (
+ /***/ 4169: /***/ (
+ module,
+ __unused_webpack_exports,
+ __webpack_require__
+ ) => {
+ module.exports = __webpack_require__(9700);
+
+ /***/
+ },
+
+ /***/ 4414: /***/ (
__unused_webpack_module,
__unused_webpack_exports,
__webpack_require__
@@ -54,7 +54,7 @@
(window.__NEXT_P = window.__NEXT_P || []).push([
"/routerDirect",
function () {
- return __webpack_require__(1640);
+ return __webpack_require__(2014);
},
]);
if (false) {
@@ -68,7 +68,7 @@
/******/ var __webpack_exec__ = (moduleId) =>
__webpack_require__((__webpack_require__.s = moduleId));
/******/ __webpack_require__.O(0, [636, 6593, 8792], () =>
- __webpack_exec__(2172)
+ __webpack_exec__(4414)
);
/******/ var __webpack_exports__ = __webpack_require__.O();
/******/ _N_E = __webpack_exports__;Diff for script-HASH.js
@@ -1,17 +1,34 @@
(self["webpackChunk_N_E"] = self["webpackChunk_N_E"] || []).push([
[1209],
{
- /***/ 4977: /***/ (
+ /***/ 2591: /***/ (
module,
__unused_webpack_exports,
__webpack_require__
) => {
- module.exports = __webpack_require__(326);
+ module.exports = __webpack_require__(4634);
/***/
},
- /***/ 5887: /***/ (
+ /***/ 5030: /***/ (
+ __unused_webpack_module,
+ __unused_webpack_exports,
+ __webpack_require__
+ ) => {
+ (window.__NEXT_P = window.__NEXT_P || []).push([
+ "/script",
+ function () {
+ return __webpack_require__(7557);
+ },
+ ]);
+ if (false) {
+ }
+
+ /***/
+ },
+
+ /***/ 7557: /***/ (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
@@ -26,7 +43,7 @@
/* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__(3108);
/* harmony import */ var next_script__WEBPACK_IMPORTED_MODULE_1__ =
- __webpack_require__(4977);
+ __webpack_require__(2591);
/* harmony import */ var next_script__WEBPACK_IMPORTED_MODULE_1___default =
/*#__PURE__*/ __webpack_require__.n(
next_script__WEBPACK_IMPORTED_MODULE_1__
@@ -58,30 +75,13 @@
/***/
},
-
- /***/ 6268: /***/ (
- __unused_webpack_module,
- __unused_webpack_exports,
- __webpack_require__
- ) => {
- (window.__NEXT_P = window.__NEXT_P || []).push([
- "/script",
- function () {
- return __webpack_require__(5887);
- },
- ]);
- if (false) {
- }
-
- /***/
- },
},
/******/ (__webpack_require__) => {
// webpackRuntimeModules
/******/ var __webpack_exec__ = (moduleId) =>
__webpack_require__((__webpack_require__.s = moduleId));
/******/ __webpack_require__.O(0, [636, 6593, 8792], () =>
- __webpack_exec__(6268)
+ __webpack_exec__(5030)
);
/******/ var __webpack_exports__ = __webpack_require__.O();
/******/ _N_E = __webpack_exports__;Diff for withRouter-HASH.js
@@ -1,34 +1,7 @@
(self["webpackChunk_N_E"] = self["webpackChunk_N_E"] || []).push([
[3263],
{
- /***/ 1179: /***/ (
- module,
- __unused_webpack_exports,
- __webpack_require__
- ) => {
- module.exports = __webpack_require__(9864);
-
- /***/
- },
-
- /***/ 2028: /***/ (
- __unused_webpack_module,
- __unused_webpack_exports,
- __webpack_require__
- ) => {
- (window.__NEXT_P = window.__NEXT_P || []).push([
- "/withRouter",
- function () {
- return __webpack_require__(4501);
- },
- ]);
- if (false) {
- }
-
- /***/
- },
-
- /***/ 4501: /***/ (
+ /***/ 559: /***/ (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
@@ -43,7 +16,7 @@
/* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__(3108);
/* harmony import */ var next_router__WEBPACK_IMPORTED_MODULE_1__ =
- __webpack_require__(1179);
+ __webpack_require__(4169);
/* harmony import */ var next_router__WEBPACK_IMPORTED_MODULE_1___default =
/*#__PURE__*/ __webpack_require__.n(
next_router__WEBPACK_IMPORTED_MODULE_1__
@@ -61,13 +34,40 @@
/***/
},
+
+ /***/ 726: /***/ (
+ __unused_webpack_module,
+ __unused_webpack_exports,
+ __webpack_require__
+ ) => {
+ (window.__NEXT_P = window.__NEXT_P || []).push([
+ "/withRouter",
+ function () {
+ return __webpack_require__(559);
+ },
+ ]);
+ if (false) {
+ }
+
+ /***/
+ },
+
+ /***/ 4169: /***/ (
+ module,
+ __unused_webpack_exports,
+ __webpack_require__
+ ) => {
+ module.exports = __webpack_require__(9700);
+
+ /***/
+ },
},
/******/ (__webpack_require__) => {
// webpackRuntimeModules
/******/ var __webpack_exec__ = (moduleId) =>
__webpack_require__((__webpack_require__.s = moduleId));
/******/ __webpack_require__.O(0, [636, 6593, 8792], () =>
- __webpack_exec__(2028)
+ __webpack_exec__(726)
);
/******/ var __webpack_exports__ = __webpack_require__.O();
/******/ _N_E = __webpack_exports__;Diff for 2161-HASH.js
Diff too large to display
Diff for 2747-HASH.js
Diff too large to display
Diff for 4322-HASH.js
failed to diffDiff for main-HASH.js
Diff too large to display
Diff for app-page-exp..ntime.dev.js
Diff too large to display
Diff for app-page-tur..ntime.dev.js
Diff too large to display
Diff for app-page-tur..ntime.dev.js
Diff too large to display
Diff for app-page.runtime.dev.js
Diff too large to display
Diff for server.runtime.prod.js
Diff too large to display
| const binaryMap = { | ||
| 'darwin-arm64': '@next/cli-darwin-arm64', | ||
| 'darwin-x64': '@next/cli-darwin-x64', | ||
| 'linux-arm64': '@next/cli-linux-arm64', | ||
| 'linux-x64': '@next/cli-linux-x64', | ||
| 'win32-x64': '@next/cli-win32-x64-msvc', | ||
| 'win32-arm64': '@next/cli-win32-arm64-msvc', | ||
| } |
There was a problem hiding this comment.
The platform-to-package mapping for Linux doesn't account for musl vs glibc variants, causing the native binary resolution to fail on Linux systems.
View Details
📝 Patch Details
diff --git a/packages/next/bin/next b/packages/next/bin/next
index 6bbc3b43ea..20458b829e 100755
--- a/packages/next/bin/next
+++ b/packages/next/bin/next
@@ -8,6 +8,40 @@ const { spawn } = require('child_process')
const platform = process.platform
const arch = process.arch
+/**
+ * Detect if the system is using musl libc instead of glibc.
+ * Returns true if musl, false if glibc.
+ */
+function isMusl() {
+ if (platform !== 'linux') {
+ return false
+ }
+
+ try {
+ // If glibcVersionRuntime is present, it's glibc (not musl)
+ const report = process.report?.getReport()
+ if (report?.header?.glibcVersionRuntime) {
+ return false
+ }
+
+ // Check for musl indicators in shared objects
+ if (Array.isArray(report?.sharedObjects)) {
+ for (const obj of report.sharedObjects) {
+ if (
+ typeof obj === 'string' &&
+ (obj.includes('libc.musl-') || obj.includes('ld-musl-'))
+ ) {
+ return true
+ }
+ }
+ }
+ } catch {
+ // Ignore errors, fall back to default
+ }
+
+ return false
+}
+
// Map to @next/cli-* package names
const binaryMap = {
'darwin-arm64': '@next/cli-darwin-arm64',
@@ -53,7 +87,13 @@ function runWithNode() {
}
const key = `${platform}-${arch}`
-const pkg = binaryMap[key]
+let pkg = binaryMap[key]
+
+// For Linux, append the libc variant (musl or gnu)
+if (pkg && platform === 'linux') {
+ const libc = isMusl() ? 'musl' : 'gnu'
+ pkg = `${pkg}-${libc}`
+}
if (pkg) {
try {
Analysis
Linux native binary resolution fails due to missing libc variant suffixes
What fails: packages/next/bin/next attempts to require @next/cli-linux-arm64 and @next/cli-linux-x64 packages that don't exist, causing require.resolve() to fail on line 62 and falling back to the Node.js implementation instead of using the native binary.
How to reproduce:
# On any Linux system (with glibc)
# The actual installed packages are:
# - @next/cli-linux-x64-gnu
# - @next/cli-linux-x64-musl
# But the code looks for:
# - @next/cli-linux-x64
node packages/next/bin/next
# This will fail to require.resolve() the package because the generic name doesn't existResult: The native binary is not used even when successfully installed. The code always falls back to running the Node.js implementation via runWithNode().
Expected: The code should detect the system's libc variant (musl or glibc) and append the appropriate suffix (-gnu or -musl) to construct the correct package name, matching the actual published packages:
@next/cli-linux-x64-gnufor glibc systems@next/cli-linux-x64-muslfor musl systems@next/cli-linux-arm64-gnufor glibc systems@next/cli-linux-arm64-muslfor musl systems
References:
- Actual package names defined in:
crates/next-cli/npm/linux-*/package.json - Installation configured in:
scripts/install-native.mjs - Reference implementation:
rspack/crates/binding/index.js(usesprocess.report?.getReport().header.glibcVersionRuntimefor detection)
Replace fork-based architecture with single Node.js process: - Add Rust CLI wrapper (`crates/next-cli/`) that handles: - NODE_OPTIONS (memory limits, source maps, inspect) - WATCHPACK_WATCHER_LIMIT for large projects - Restart loop (exit code 77) without Node.js overhead - Simplify next-dev.ts: remove fork(), IPC, child process management - Simplify start-server.ts: remove NEXT_PRIVATE_WORKER handling - Add shell entry points (bin/next, bin/next.cmd) that: - Use native binary when available (~10ms overhead) - Fall back to Node.js entry point - Add lazy loading for heavy modules: - tar (download-swc) - only needed for fallback download - edge-runtime sandbox - only needed for edge functions - MCP SDK - only needed when clients connect - Set up cross-platform release pipeline: - Build @next/cli-* for 8 platforms in CI - Publish as optional dependencies - Update install-native.mjs for development Saves ~35-50ms on dev server startup by eliminating parent Node.js process and fork overhead. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Yarn Berry PnP doesn't support shell script binaries - it tries to parse them as JavaScript. Convert bin/next from shell script to JavaScript with #!/usr/bin/env node shebang. The JS wrapper: - Detects platform/arch - Looks for Rust CLI binary in @next/cli-* packages - Falls back to dist/bin/next if not found Benchmark: ~25-35ms overhead vs shell script (negligible for 500-1500ms startup) Also removes next.cmd as npm/yarn auto-generate .cmd wrappers for JS bins.
❌ 119 failing tests in 27 jobsUpdated 2026-01-05 10:01:41 UTC · Commit: f02153b Details📁
|

Summary
Replace fork-based architecture with single Node.js process:
Add Rust CLI wrapper (
crates/next-cli/) that handles:Simplify next-dev.ts: remove fork(), IPC, child process management
Simplify start-server.ts: remove NEXT_PRIVATE_WORKER handling
Add shell entry points (bin/next, bin/next.cmd) that:
Cross-platform release pipeline for 8 platforms
Test Fixes
The Rust CLI introduces exit code 77 for server restarts (e.g., after config file changes). Tests run Node.js directly without the Rust wrapper, so:
test/lib/next-modes/next-dev.ts- AddRESTART_EXIT_CODE = 77handling inNextDevInstance.start()to auto-restart the server when it exits with code 77, mimicking the Rust wrapper behaviorBenchmark Results (cumulative)
Biggest impact of all optimizations - eliminates Node.js fork overhead.
Test Plan