Skip to content

Commit 5ac9f6c

Browse files
committed
fix: convert bin/next to JavaScript for Yarn PnP compatibility
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.
1 parent f713fa0 commit 5ac9f6c

File tree

3 files changed

+146
-72
lines changed

3 files changed

+146
-72
lines changed

packages/next/bin/next

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,82 @@
1-
#!/bin/sh
2-
# Next.js CLI entry point
3-
#
4-
# Tries to use the native Rust binary for faster restarts, falls back to Node.js.
5-
# The Rust binary is distributed via @next/cli-* platform-specific packages.
1+
#!/usr/bin/env node
2+
// Next.js CLI entry point
3+
// Tries native Rust binary first, falls back to Node.js implementation
64

7-
DIR="$(dirname "$0")"
5+
const { dirname, join } = require('path')
6+
const { spawn } = require('child_process')
87

9-
# Production: check installed @next/cli-* package (hoisted to node_modules/@next/cli-*)
10-
for pkg in "$DIR"/../../@next/cli-*/next; do
11-
[ -x "$pkg" ] && exec "$pkg" "$@"
12-
done
8+
const platform = process.platform
9+
const arch = process.arch
1310

14-
# Fallback: Node.js (also used for in-repo development)
15-
exec node "$DIR/../dist/bin/next" "$@"
11+
// Map to @next/cli-* package names
12+
const binaryMap = {
13+
'darwin-arm64': '@next/cli-darwin-arm64',
14+
'darwin-x64': '@next/cli-darwin-x64',
15+
'linux-arm64': '@next/cli-linux-arm64',
16+
'linux-x64': '@next/cli-linux-x64',
17+
'win32-x64': '@next/cli-win32-x64-msvc',
18+
'win32-arm64': '@next/cli-win32-arm64-msvc',
19+
}
20+
21+
/**
22+
* Fallback: spawn Node.js with the same flags the Rust binary would use.
23+
* This ensures feature parity when the native binary isn't available.
24+
*/
25+
function runWithNode() {
26+
const args = process.argv.slice(2)
27+
const nodeArgs = ['--enable-source-maps']
28+
29+
// Parse args to handle --disable-source-maps like the Rust binary
30+
const hasDisableSourceMaps = args.includes('--disable-source-maps')
31+
if (hasDisableSourceMaps) {
32+
nodeArgs.length = 0 // Clear the array - don't pass --enable-source-maps
33+
}
34+
35+
// Extract --inspect flags (they must come before the script for Node.js)
36+
const inspectArgs = []
37+
const scriptArgs = []
38+
for (const arg of args) {
39+
if (arg.startsWith('--inspect')) {
40+
inspectArgs.push(arg)
41+
} else {
42+
scriptArgs.push(arg)
43+
}
44+
}
45+
46+
const scriptPath = join(__dirname, '../dist/bin/next')
47+
const child = spawn(
48+
process.execPath,
49+
[...nodeArgs, ...inspectArgs, scriptPath, ...scriptArgs],
50+
{ stdio: 'inherit', env: process.env }
51+
)
52+
child.on('close', (code) => process.exit(code ?? 0))
53+
}
54+
55+
const key = `${platform}-${arch}`
56+
const pkg = binaryMap[key]
57+
58+
if (pkg) {
59+
try {
60+
// Use require.resolve() for robust package resolution
61+
// Works with npm, yarn, pnpm, and various hoisting strategies
62+
const pkgPath = require.resolve(`${pkg}/package.json`)
63+
const binName = platform === 'win32' ? 'next.exe' : 'next'
64+
const binPath = join(dirname(pkgPath), binName)
65+
66+
const child = spawn(binPath, process.argv.slice(2), {
67+
stdio: 'inherit',
68+
env: process.env,
69+
})
70+
child.on('close', (code) => process.exit(code ?? 0))
71+
child.on('error', () => {
72+
// Binary exists but failed to execute, fall through to JS with Node flags
73+
runWithNode()
74+
})
75+
} catch {
76+
// Package not installed, use JS implementation with Node flags
77+
runWithNode()
78+
}
79+
} else {
80+
// Unsupported platform, use JS implementation with Node flags
81+
runWithNode()
82+
}

packages/next/bin/next.cmd

Lines changed: 0 additions & 22 deletions
This file was deleted.

test/lib/next-modes/next-dev.ts

Lines changed: 67 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import { retry, waitFor } from 'next-test-utils'
55
import stripAnsi from 'strip-ansi'
66
import { quote as shellQuote } from 'shell-quote'
77

8+
// Exit code 77 signals that the server wants to be restarted (e.g., after config changes)
9+
// The Rust CLI wrapper normally handles this, but in tests we need to handle it ourselves
10+
const RESTART_EXIT_CODE = 77
11+
812
export class NextDevInstance extends NextInstance {
913
private _cliOutput: string = ''
1014

@@ -150,54 +154,78 @@ export class NextDevInstance extends NextInstance {
150154
}
151155
}
152156

157+
const spawnServer = () => {
158+
this.childProcess = spawn(startArgs[0], startArgs.slice(1), {
159+
cwd: this.testDir,
160+
stdio: ['ignore', 'pipe', 'pipe'],
161+
shell: false,
162+
env: {
163+
...process.env,
164+
...this.env,
165+
NODE_ENV: this.env.NODE_ENV || ('' as any),
166+
PORT: this.forcedPort || '0',
167+
__NEXT_TEST_MODE: 'e2e',
168+
},
169+
})
170+
171+
this.childProcess.stdout!.on('data', (chunk) => {
172+
const msg = chunk.toString()
173+
process.stdout.write(chunk)
174+
this._cliOutput += msg
175+
this.emit('stdout', [msg])
176+
})
177+
this.childProcess.stderr!.on('data', (chunk) => {
178+
const msg = chunk.toString()
179+
process.stderr.write(chunk)
180+
this._cliOutput += msg
181+
this.emit('stderr', [msg])
182+
})
183+
184+
return this.childProcess
185+
}
186+
153187
console.log('running', shellQuote(startArgs))
154188
await new Promise<void>((resolve, reject) => {
155189
try {
156-
this.childProcess = spawn(startArgs[0], startArgs.slice(1), {
157-
cwd: this.testDir,
158-
stdio: ['ignore', 'pipe', 'pipe'],
159-
shell: false,
160-
env: {
161-
...process.env,
162-
...this.env,
163-
NODE_ENV: this.env.NODE_ENV || ('' as any),
164-
PORT: this.forcedPort || '0',
165-
__NEXT_TEST_MODE: 'e2e',
166-
},
167-
})
168-
190+
spawnServer()
169191
this._cliOutput = ''
170192

171-
this.childProcess.stdout!.on('data', (chunk) => {
172-
const msg = chunk.toString()
173-
process.stdout.write(chunk)
174-
this._cliOutput += msg
175-
this.emit('stdout', [msg])
176-
})
177-
this.childProcess.stderr!.on('data', (chunk) => {
178-
const msg = chunk.toString()
179-
process.stderr.write(chunk)
180-
this._cliOutput += msg
181-
this.emit('stderr', [msg])
182-
})
183-
184193
const serverReadyTimeoutId = this.setServerReadyTimeout(
185194
reject,
186195
this.startServerTimeout
187196
)
188197

189-
this.childProcess.on('close', (code, signal) => {
190-
if (this.isStopping) return
191-
if (code || signal) {
192-
this.childProcess = undefined
193-
const error = new Error(
194-
`next dev exited unexpectedly with code/signal ${code || signal}`
195-
)
196-
clearTimeout(serverReadyTimeoutId)
197-
require('console').error(error)
198-
reject(error)
199-
}
200-
})
198+
let isInitialStartup = true
199+
200+
const setupCloseHandler = () => {
201+
this.childProcess!.on('close', (code, signal) => {
202+
if (this.isStopping) return
203+
204+
// Handle restart exit code (77) - the Rust wrapper normally handles this,
205+
// but in tests we need to restart the server ourselves
206+
if (code === RESTART_EXIT_CODE) {
207+
spawnServer()
208+
setupCloseHandler()
209+
return
210+
}
211+
212+
if (code || signal) {
213+
this.childProcess = undefined
214+
const error = new Error(
215+
`next dev exited unexpectedly with code/signal ${code || signal}`
216+
)
217+
if (isInitialStartup) {
218+
clearTimeout(serverReadyTimeoutId)
219+
require('console').error(error)
220+
reject(error)
221+
} else {
222+
// After initial startup, log but don't reject (promise already resolved)
223+
require('console').error(error)
224+
}
225+
}
226+
})
227+
}
228+
setupCloseHandler()
201229

202230
const readyCb = (msg) => {
203231
const resolveServer = () => {
@@ -211,6 +239,7 @@ export class NextDevInstance extends NextInstance {
211239
})
212240
}
213241
// server might reload so we keep listening
242+
isInitialStartup = false
214243
resolve()
215244
}
216245

0 commit comments

Comments
 (0)