Files
vercel__next.js/test/e2e/cli/cli.test.ts
Marcos Hernanz 090f1b7275 fix: suppress missing package manager errors in next info (#97946)
## What

- capture stdout and stderr for both direct and shell-fallback
package-manager version probes
- keep successful version output unchanged and return `N/A` when a probe
fails
- add deterministic cross-platform regression coverage with a failing
Yarn shim

Closes #97932

## Why

PR #71134 added an `execSync` fallback so package-manager command shims
can be resolved on Windows. When `stdio` is omitted, Node mirrors stderr
from failed synchronous child processes to the parent, so an unavailable
Yarn or pnpm can print raw shell errors before `next info` reports
`N/A`.

Passing `{ stdio: "pipe" }` to both probes preserves captured stdout and
the Windows fallback while keeping expected probe failures silent. The
change stays scoped to package-manager version detection; shared
registry discovery is unchanged.

## Testing

- `pnpm --filter next typescript`
- `pnpm --filter next build`
- `pnpm test-types --pretty false`
- ESLint and Prettier on both changed files
- `pnpm test-start test/e2e/cli/cli.test.ts` (66 passed, 1 skipped)
- built-CLI reproduction with npm available and Yarn/pnpm absent: both
report `N/A` with no Yarn/pnpm command-not-found output
- autoreview with GPT-5.6 Sol xhigh and Claude Opus 5 xhigh: zero
findings

---------

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
2026-09-03 18:12:15 -07:00

1289 lines
38 KiB
TypeScript

import { nextTestSetup } from 'e2e-utils'
import { findPort, killApp, retry } from 'next-test-utils'
import path, { join } from 'path'
// @ts-expect-error
import pkg from 'next/package'
import http from 'http'
import { promises as fs } from 'fs'
import stripAnsi from 'strip-ansi'
import type { ChildProcess } from 'child_process'
const itCI = process.env.NEXT_TEST_CI ? it : it.skip
const reactDependencies = {
react: '19.3.0-canary-fef12a01-20260413',
'react-dom': '19.3.0-canary-fef12a01-20260413',
}
describe('CLI Usage', () => {
const { next, isNextStart } = nextTestSetup({
files: join(__dirname, 'basic'),
skipStart: true,
dependencies: reactDependencies,
skipDeployment: true,
})
/**
* Start a long-running Next.js CLI command (e.g. `next dev`, `next start`)
* and return a handle for externally killing it.
*
* Waits for `readyPattern` to match in stdout before resolving so callers can
* make assertions against a running server. If the process exits before the
* pattern matches, the returned promise resolves anyway and the caller can
* inspect the captured output.
*/
async function launchDevServer(
args: string[],
opts: {
env?: Record<string, string>
cwd?: string
onStdout?: (msg: string) => void
onStderr?: (msg: string) => void
/** Pattern signaling the server is ready. */
readyPattern?: RegExp
} = {}
): Promise<{ child: ChildProcess; exit: Promise<any> }> {
const readyPattern = opts.readyPattern ?? /- Local:|✓ Ready/i
let child!: ChildProcess
let ready = false
let resolveReady!: () => void
const readyPromise = new Promise<void>((r) => {
resolveReady = () => {
if (!ready) {
ready = true
r()
}
}
})
const exit = next
.runCommand(args, {
env: opts.env,
cwd: opts.cwd,
onStdout: (msg) => {
opts.onStdout?.(msg)
if (readyPattern.test(stripAnsi(msg))) resolveReady()
},
onStderr: (msg) => {
opts.onStderr?.(msg)
if (readyPattern.test(stripAnsi(msg))) resolveReady()
},
instance: (p) => {
child = p
},
})
.finally(() => {
resolveReady()
})
await readyPromise
return { child, exit }
}
const runAndCaptureOutput = async ({ port }: { port: number }) => {
let stdout = ''
let stderr = ''
let app = http.createServer((_, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('OK')
})
await new Promise<void>((resolve, reject) => {
app.on('error', reject)
app.on('listening', () => resolve())
app.listen(port)
})
const { child, exit } = await launchDevServer(
['dev', next.testDir, '-p', String(port)],
{
onStdout(msg) {
stdout += msg
},
onStderr(msg) {
stderr += msg
},
}
)
try {
// Give the server a moment to emit the port-in-use error if it hasn't
// already been emitted before the ready pattern matched.
await retry(
() => {
if (!stderr.includes('already in use') && child.exitCode === null) {
throw new Error('waiting for port conflict error')
}
},
5000,
100
).catch(() => {})
} finally {
await killApp(child).catch(() => {})
await exit.catch(() => {})
}
await new Promise((resolve) => app.close(resolve))
return { stdout, stderr }
}
const testExitSignal = async (
killSignal: NodeJS.Signals | '' = '',
args: string[] = [],
readyRegex = /Creating an optimized production/,
expectedCode = 0
) => {
let instance: ChildProcess | undefined
let output = ''
const cmdPromise = next
.runCommand(args, {
ignoreFail: true,
instance: (p) => {
instance = p
},
onStdout: (msg) => {
output += stripAnsi(msg)
},
})
.catch((err) => expect.fail(err.message))
await retry(() => {
expect(output).toMatch(readyRegex)
})
instance!.kill(killSignal as NodeJS.Signals)
const { code, signal } = await cmdPromise
// Node can only partially emulate signals on Windows. Our signal handlers won't affect the exit code.
// See: https://nodejs.org/api/process.html#process_signal_events
const expectedExitSignal = process.platform === `win32` ? killSignal : null
expect(signal).toBe(expectedExitSignal)
expect(code).toBe(expectedCode)
}
;(isNextStart ? describe : describe.skip)('production mode', () => {
describe('start', () => {
test('should exit when SIGINT is signalled', async () => {
require('console').log('before build')
await next.deleteFile('.next')
await next.build()
require('console').log('build finished')
const port = await findPort()
await testExitSignal(
'SIGINT',
['start', next.testDir, '-p', String(port)],
/- Local:/,
130 // 128 + 2 (SIGINT)
)
})
test('should exit when SIGTERM is signalled', async () => {
await next.deleteFile('.next')
await next.build()
const port = await findPort()
await testExitSignal(
'SIGTERM',
['start', next.testDir, '-p', String(port)],
/- Local:/,
143 // 128 + 15 (SIGTERM)
)
})
test('--help', async () => {
const help = await next.runCommand(['start', '--help'])
expect(help.stdout).toMatch(/Starts Next.js in production mode/)
})
test('-h', async () => {
const help = await next.runCommand(['start', '-h'])
expect(help.stdout).toMatch(/Starts Next.js in production mode/)
})
test('should format IPv6 addresses correctly', async () => {
await next.build()
const port = await findPort()
let stdout = ''
const { child, exit } = await launchDevServer(
['start', next.testDir, '--hostname', '::', '--port', String(port)],
{
onStdout(msg) {
stdout += msg
},
}
)
try {
await retry(() => {
// Only display when hostname is provided
expect(stdout).toMatch(
new RegExp(`Network:\\s*http://\\[::\\]:${port}`)
)
expect(stdout).toMatch(new RegExp(`http://\\[::1\\]:${port}`))
})
} finally {
await killApp(child)
await exit.catch(() => {})
}
})
test('should warn when unknown argument provided', async () => {
const { stderr } = await next.runCommand(['start', '--random'])
expect(stderr).toEqual(`error: unknown option '--random'\n`)
})
test('should not throw UnhandledPromiseRejectionWarning', async () => {
const { stderr } = await next.runCommand(['start', '--random'])
expect(stderr).not.toContain('UnhandledPromiseRejectionWarning')
})
test('invalid directory', async () => {
const output = await next.runCommand(['start', 'non-existent'])
expect(output.stderr).toContain(
'Invalid project directory provided, no such directory'
)
})
test('--keepAliveTimeout string arg', async () => {
const { stderr } = await next.runCommand([
'start',
'--keepAliveTimeout',
'string',
])
expect(stderr).toContain(
`error: option '--keepAliveTimeout <keepAliveTimeout>' argument 'string' is invalid. 'string' is not a non-negative number.`
)
})
test('--keepAliveTimeout negative number', async () => {
const { stderr } = await next.runCommand([
'start',
'--keepAliveTimeout=-100',
])
expect(stderr).toContain(
`error: option '--keepAliveTimeout <keepAliveTimeout>' argument '-100' is invalid. '-100' is not a non-negative number.`
)
})
test('--keepAliveTimeout Infinity', async () => {
const { stderr } = await next.runCommand([
'start',
'--keepAliveTimeout',
'Infinity',
])
expect(stderr).toContain(
`error: option '--keepAliveTimeout <keepAliveTimeout>' argument 'Infinity' is invalid. 'Infinity' is not a non-negative number.`
)
})
test('--keepAliveTimeout happy path', async () => {
await next.build()
const port = await findPort()
let stderr = ''
const { child, exit } = await launchDevServer(
[
'start',
next.testDir,
'--keepAliveTimeout',
'100',
'--port',
String(port),
],
{
onStderr(msg) {
stderr += msg
},
}
)
try {
expect(stderr).not.toContain(
`error: option '--keepAliveTimeout <keepAliveTimeout>' argument '100' is invalid. '100' is not a non-negative number.`
)
} finally {
await killApp(child)
await exit.catch(() => {})
}
})
test('should not start on a port out of range', async () => {
const invalidPort = '300001'
const { stderr } = await next.runCommand([
'start',
'--port',
invalidPort,
])
expect(stderr).toContain(`options.port should be >= 0 and < 65536.`)
})
test('should not start on a reserved port', async () => {
const reservedPort = '4045'
const { stderr } = await next.runCommand([
'start',
'--port',
reservedPort,
])
expect(stderr).toContain(
`Bad port: "${reservedPort}" is reserved for npp`
)
})
test('--inspect', async () => {
await next.build()
const port = await findPort()
let output = ''
let errOutput = ''
const { child, exit } = await launchDevServer(
['start', next.testDir, '--port', String(port), '--inspect'],
{
onStdout(msg) {
output += stripAnsi(msg)
},
onStderr(msg) {
errOutput += stripAnsi(msg)
},
}
)
try {
await retry(() => {
expect(output).toMatch(new RegExp(`http://localhost:${port}`))
})
await retry(() => {
expect(output).toMatch(/- Debugger port:\s+9229/)
})
await retry(() => {
expect(errOutput).toMatch(/Debugger listening on/)
})
expect(errOutput).not.toContain('address already in use')
} finally {
await killApp(child)
await exit.catch(() => {})
}
})
})
describe('telemetry', () => {
test('--help', async () => {
const help = await next.runCommand(['telemetry', '--help'])
expect(help.stdout).toMatch(/Allows you to enable or disable Next\.js'/)
})
test('-h', async () => {
const help = await next.runCommand(['telemetry', '-h'])
expect(help.stdout).toMatch(/Allows you to enable or disable Next\.js'/)
})
test('should warn when unknown argument provided', async () => {
const { stderr } = await next.runCommand(['telemetry', '--random'])
expect(stderr).toEqual(`error: unknown option '--random'\n`)
})
test('should not throw UnhandledPromiseRejectionWarning', async () => {
const { stderr } = await next.runCommand(['telemetry', '--random'])
expect(stderr).not.toContain('UnhandledPromiseRejectionWarning')
})
})
describe('build', () => {
test('--help', async () => {
const help = await next.runCommand(['build', '--help'])
expect(help.stdout).toMatch(/Creates an optimized production build/)
})
test('-h', async () => {
const help = await next.runCommand(['build', '-h'])
expect(help.stdout).toMatch(/Creates an optimized production build/)
})
test('should warn when unknown argument provided', async () => {
const { stderr } = await next.runCommand(['build', '--random'])
expect(stderr).toEqual(`error: unknown option '--random'\n`)
})
test('should not throw UnhandledPromiseRejectionWarning', async () => {
const { stderr } = await next.runCommand(['build', '--random'])
expect(stderr).not.toContain('UnhandledPromiseRejectionWarning')
})
test('should exit when SIGINT is signalled', async () => {
await testExitSignal('SIGINT', ['build', next.testDir], undefined, 130)
})
test('should exit when SIGTERM is signalled', async () => {
await testExitSignal('SIGTERM', ['build', next.testDir], undefined, 143)
})
test('invalid directory', async () => {
const output = await next.runCommand(['build', 'non-existent'])
expect(output.stderr).toContain(
'Invalid project directory provided, no such directory'
)
})
})
})
describe('no command', () => {
test('--help', async () => {
const help = await next.runCommand(['--help'])
expect(help.stdout).toMatch(
/The Next.js CLI allows you to develop, build, start/
)
})
test('-h', async () => {
const help = await next.runCommand(['-h'])
expect(help.stdout).toMatch(
/The Next.js CLI allows you to develop, build, start/
)
})
test('--version', async () => {
const output = await next.runCommand(['--version'])
expect(output.stdout).toMatch(
new RegExp(`Next\\.js v${pkg.version.replace(/\./g, '\\.')}`)
)
})
test('-v', async () => {
const output = await next.runCommand(['--version'])
expect(output.stdout).toMatch(
new RegExp(`Next\\.js v${pkg.version.replace(/\./g, '\\.')}`)
)
})
test('invalid directory', async () => {
const output = await next.runCommand(['non-existent'])
expect(output.stderr).toContain(
'Invalid project directory provided, no such directory'
)
})
test('detects command typos', async () => {
const typos = [
['buidl', 'build'],
['buill', 'build'],
['biild', 'build'],
['starr', 'start'],
['dee', 'dev'],
]
for (const typo of typos) {
const output = await next.runCommand([typo[0]])
expect(output.stderr).toContain(
`"next ${typo[0]}" does not exist. Did you mean "next ${typo[1]}"?`
)
}
})
})
describe('dev', () => {
test('--help', async () => {
const help = await next.runCommand(['dev', '--help'])
expect(help.stdout).toMatch(/Starts Next.js in development mode/)
})
test('-h', async () => {
const help = await next.runCommand(['dev', '-h'])
expect(help.stdout).toMatch(/Starts Next.js in development mode/)
})
test('custom directory', async () => {
const port = await findPort()
let output = ''
const { child, exit } = await launchDevServer(
['dev', next.testDir, '--port', String(port)],
{
onStdout(msg) {
output += stripAnsi(msg)
},
}
)
try {
await retry(() => {
expect(output).toMatch(/- Local:/i)
})
} finally {
await killApp(child)
await exit.catch(() => {})
}
})
test('--port', async () => {
const port = await findPort()
let output = ''
const { child, exit } = await launchDevServer(
['dev', next.testDir, '--port', String(port)],
{
onStdout(msg) {
output += stripAnsi(msg)
},
}
)
try {
await retry(() => {
expect(output).toMatch(new RegExp(`http://localhost:${port}`))
})
await retry(() => {
expect(output).toMatch(
/Network:\s*http:\/\/[\d]{1,}\.[\d]{1,}\.[\d]{1,}/
)
})
} finally {
await killApp(child)
await exit.catch(() => {})
}
})
test('--port 0', async () => {
const port = await findPort()
let output = ''
const { child, exit } = await launchDevServer(
['dev', next.testDir, '--port', String(port)],
{
onStdout(msg) {
output += stripAnsi(msg)
},
}
)
try {
await retry(() => {
expect(output).toMatch(new RegExp(`http://localhost:${port}`))
})
await retry(() => {
expect(output).toMatch(
/Network:\s*http:\/\/[\d]{1,}\.[\d]{1,}\.[\d]{1,}/
)
})
} finally {
await killApp(child)
await exit.catch(() => {})
}
const matches = /- Local/.exec(output)
expect(matches).not.toBe(null)
const _port = parseInt(matches![1])
// Regression test: port 0 was interpreted as if no port had been
// provided, falling back to 3000.
expect(_port).not.toBe(3000)
})
test('PORT=0', async () => {
let output = ''
const { child, exit } = await launchDevServer(['dev', next.testDir], {
env: {
PORT: '0',
},
onStdout(msg) {
output += stripAnsi(msg)
},
})
try {
await retry(() => {
expect(output).toMatch(/- Local:/)
})
// without --hostname, do not log Network: xxx
const matches = /Network:\s*http:\/\/\[::\]:(\d+)/.exec(output)
const _port = parseInt('' + matches)
expect(matches).toBe(null)
// Regression test: port 0 was interpreted as if no port had been
// provided, falling back to 3000.
expect(_port).not.toBe(3000)
} finally {
await killApp(child)
await exit.catch(() => {})
}
})
test("NODE_OPTIONS='--inspect'", async () => {
const port = await findPort()
let output = ''
let errOutput = ''
const { child, exit } = await launchDevServer(
['dev', next.testDir, '--port', String(port)],
{
onStdout(msg) {
output += stripAnsi(msg)
},
onStderr(msg) {
errOutput += stripAnsi(msg)
},
env: { NODE_OPTIONS: '--inspect' },
}
)
try {
await retry(() => {
expect(output).toMatch(new RegExp(`http://localhost:${port}`))
})
await retry(() => {
expect(output).toMatch(/- Debugger port:\s+\d+/)
})
await retry(() => {
expect(errOutput).toMatch(/Debugger listening on/)
})
expect(errOutput).not.toContain('address already in use')
} finally {
await killApp(child)
await exit.catch(() => {})
}
})
test('--inspect', async () => {
const port = await findPort()
let output = ''
let errOutput = ''
const { child, exit } = await launchDevServer(
['dev', next.testDir, '--port', String(port), '--inspect'],
{
onStdout(msg) {
output += stripAnsi(msg)
},
onStderr(msg) {
errOutput += stripAnsi(msg)
},
}
)
try {
await retry(() => {
expect(output).toMatch(new RegExp(`http://localhost:${port}`))
})
await retry(() => {
expect(output).toMatch(/- Debugger port:\s+9229/)
})
await retry(() => {
expect(errOutput).toMatch(/Debugger listening on/)
})
expect(errOutput).not.toContain('address already in use')
} finally {
await killApp(child)
await exit.catch(() => {})
}
})
test('--inspect 0', async () => {
const port = await findPort()
let output = ''
let errOutput = ''
const { child, exit } = await launchDevServer(
['dev', next.testDir, '--port', String(port), '--inspect', '0'],
{
onStdout(msg) {
output += stripAnsi(msg)
},
onStderr(msg) {
errOutput += stripAnsi(msg)
},
}
)
try {
await retry(() => {
expect(output).toMatch(new RegExp(`http://localhost:${port}`))
})
await retry(() => {
expect(output).toMatch(/- Debugger port:\s+\d+/)
})
await retry(() => {
expect(errOutput).toMatch(/Debugger listening on/)
})
expect(errOutput).not.toContain('address already in use')
} finally {
await killApp(child)
await exit.catch(() => {})
}
})
test('--inspect [port]', async () => {
const port = await findPort()
let output = ''
let errOutput = ''
const { child, exit } = await launchDevServer(
['dev', next.testDir, '--port', String(port), '--inspect', '9230'],
{
onStdout(msg) {
output += stripAnsi(msg)
},
onStderr(msg) {
errOutput += stripAnsi(msg)
},
}
)
try {
await retry(() => {
expect(output).toMatch(new RegExp(`http://localhost:${port}`))
})
await retry(() => {
expect(output).toMatch(/- Debugger port:\s+9230/)
})
await retry(() => {
expect(errOutput).toMatch(/Debugger listening on/)
})
expect(errOutput).not.toContain('address already in use')
} finally {
await killApp(child)
await exit.catch(() => {})
}
})
test("NODE_OPTIONS='--inspect=:0'", async () => {
const port = await findPort()
let output = ''
let errOutput = ''
const { child, exit } = await launchDevServer(
['dev', next.testDir, '--port', String(port)],
{
onStdout(msg) {
output += stripAnsi(msg)
},
onStderr(msg) {
errOutput += stripAnsi(msg)
},
env: { NODE_OPTIONS: '--inspect=:0' },
}
)
try {
await retry(() => {
expect(output).toMatch(new RegExp(`http://localhost:${port}`))
})
await retry(() => {
expect(output).toMatch(/- Debugger port:\s+\d+/)
})
await retry(() => {
expect(errOutput).toMatch(/Debugger listening on/)
})
expect(errOutput).not.toContain('address already in use')
expect(errOutput).toContain('Debugger listening on')
console.log(output)
} finally {
await killApp(child)
await exit.catch(() => {})
}
})
test("NODE_OPTIONS='--require=file with spaces to-require-with-node-require-option.js'", async () => {
const port = await findPort()
let output = ''
let errOutput = ''
const { child, exit } = await launchDevServer(
['dev', next.testDir, '--port', String(port)],
{
cwd: next.testDir,
onStdout(msg) {
output += stripAnsi(msg)
},
onStderr(msg) {
errOutput += stripAnsi(msg)
},
env: {
NODE_OPTIONS:
'--require "./file with spaces to-require-with-node-require-option.js"',
},
}
)
try {
await retry(() => {
expect(output).toMatch(new RegExp(`http://localhost:${port}`))
})
expect(output).toContain(
'FILE_WITH_SPACES_TO_REQUIRE_WITH_NODE_REQUIRE_OPTION'
)
expect(errOutput).toBe('')
} finally {
await killApp(child)
await exit.catch(() => {})
}
})
test("NODE_OPTIONS='--require=file with spaces to --require.js'", async () => {
const port = await findPort()
let output = ''
let errOutput = ''
const { child, exit } = await launchDevServer(
['dev', next.testDir, '--port', String(port)],
{
cwd: next.testDir,
onStdout(msg) {
output += stripAnsi(msg)
},
onStderr(msg) {
errOutput += stripAnsi(msg)
},
env: {
NODE_OPTIONS: '--require "./file with spaces to --require.js"',
},
}
)
try {
await retry(() => {
expect(output).toMatch(new RegExp(`http://localhost:${port}`))
})
expect(output).toContain(
'FILE_WITH_SPACES_TO_REQUIRE_WITH_NODE_REQUIRE_OPTION'
)
expect(errOutput).toBe('')
} finally {
await killApp(child)
await exit.catch(() => {})
}
})
test('-p', async () => {
const port = await findPort()
let output = ''
const { child, exit } = await launchDevServer(
['dev', next.testDir, '-p', String(port)],
{
onStdout(msg) {
output += stripAnsi(msg)
},
env: { NODE_OPTIONS: '--inspect' },
}
)
try {
await retry(() => {
expect(output).toMatch(new RegExp(`http://localhost:${port}`))
})
} finally {
await killApp(child)
await exit.catch(() => {})
}
})
test('-p conflict', async () => {
const port = await findPort()
const { stderr, stdout } = await runAndCaptureOutput({ port })
expect(stderr).toMatch('already in use')
expect(stdout).not.toMatch(/ready/i)
expect(stdout).not.toMatch('started')
expect(stdout).not.toMatch(`${port}`)
expect(stripAnsi(stdout).trim()).toBeFalsy()
})
test('Allow retry if default port is already in use', async () => {
let output = ''
let one: { child: ChildProcess; exit: Promise<any> } | undefined
let two: { child: ChildProcess; exit: Promise<any> } | undefined
try {
one = await launchDevServer(['dev', next.testDir], {})
two = await launchDevServer(['dev', next.testDir], {
onStderr(msg) {
output += stripAnsi(msg)
},
})
} finally {
if (one) {
await killApp(one.child).catch(console.error)
await one.exit.catch(() => {})
}
if (two) {
await killApp(two.child).catch(console.error)
await two.exit.catch(() => {})
}
}
// Depending on the environment Next may or may not be able to resolve
// the process holding the port; accept both wordings.
expect(output).toMatch(
/⚠ Port 3000 is in use by (an unknown process|process \d+), using available port \d+ instead\./
)
})
test('-p reserved', async () => {
const TCP_MUX_PORT = 1
// Don't pre-bind a server to port 1 here: Next.js rejects reserved
// ports during CLI argument parsing, before attempting to bind, so the
// pre-bind step is unnecessary and would also fail on CI where binding
// privileged ports (< 1024) requires root.
let stdout = ''
let stderr = ''
await next.runCommand(['dev', next.testDir, '-p', String(TCP_MUX_PORT)], {
ignoreFail: true,
onStdout: (msg) => {
stdout += msg
},
onStderr: (msg) => {
stderr += msg
},
})
expect(stdout).toMatch('')
expect(stderr).toMatch(
`Bad port: "${TCP_MUX_PORT}" is reserved for tcpmux`
)
})
test('--hostname', async () => {
const port = await findPort()
let output = ''
const { child, exit } = await launchDevServer(
['dev', next.testDir, '--hostname', '0.0.0.0', '--port', String(port)],
{
onStdout(msg) {
output += stripAnsi(msg)
},
}
)
try {
await retry(() => {
expect(output).toMatch(
new RegExp(`Network:\\s*http://0.0.0.0:${port}`)
)
})
await retry(() => {
expect(output).toMatch(new RegExp(`http://localhost:${port}`))
})
} finally {
await killApp(child)
await exit.catch(() => {})
}
})
test('-H', async () => {
const port = await findPort()
let output = ''
const { child, exit } = await launchDevServer(
['dev', next.testDir, '-H', '0.0.0.0', '--port', String(port)],
{
onStdout(msg) {
output += stripAnsi(msg)
},
}
)
try {
await retry(() => {
expect(output).toMatch(
new RegExp(`Network:\\s*http://0.0.0.0:${port}`)
)
})
await retry(() => {
expect(output).toMatch(new RegExp(`http://localhost:${port}`))
})
} finally {
await killApp(child)
await exit.catch(() => {})
}
})
itCI('--experimental-https', async () => {
// only runs on CI as it requires administrator privileges
const port = await findPort()
let output = ''
const { child, exit } = await launchDevServer(
['dev', next.testDir, '--experimental-https', '--port', String(port)],
{
onStdout(msg) {
output += stripAnsi(msg)
},
}
)
try {
await retry(() => {
expect(output).toMatch(/Network:\s*https:\/\//)
})
expect(output).toMatch(/Local:\s*https:\/\/localhost:(\d+)/)
expect(output).toContain('Certificates created in')
} finally {
await killApp(child)
await exit.catch(() => {})
}
})
test('--experimental-https with provided key/cert', async () => {
const keyFile = path.resolve(__dirname, 'certificates/localhost-key.pem')
const certFile = path.resolve(__dirname, 'certificates/localhost.pem')
const port = await findPort()
let output = ''
const { child, exit } = await launchDevServer(
[
'dev',
next.testDir,
'--experimental-https',
'--experimental-https-key',
keyFile,
'--experimental-https-cert',
certFile,
'--port',
String(port),
],
{
onStdout(msg) {
output += stripAnsi(msg)
},
}
)
try {
await retry(() => {
expect(output).toMatch(/https:\/\/localhost:(\d+)/)
})
} finally {
await killApp(child)
await exit.catch(() => {})
}
})
test('should format IPv6 addresses correctly', async () => {
const port = await findPort()
let output = ''
const { child, exit } = await launchDevServer(
['dev', next.testDir, '--hostname', '::', '--port', String(port)],
{
onStdout(msg) {
output += stripAnsi(msg)
},
}
)
try {
// Only display when hostname is provided
await retry(() => {
expect(output).toMatch(
new RegExp(`Network:\\s*\\http://\\[::\\]:${port}`)
)
})
await retry(() => {
expect(output).toMatch(new RegExp(`http://\\[::1\\]:${port}`))
})
} finally {
await killApp(child).catch(() => {})
await exit.catch(() => {})
}
})
test('should warn when unknown argument provided', async () => {
const { stderr } = await next.runCommand(['dev', '--random'])
expect(stderr).toEqual(`error: unknown option '--random'\n`)
})
test('should not throw UnhandledPromiseRejectionWarning', async () => {
const { stderr } = await next.runCommand(['dev', '--random'])
expect(stderr).not.toContain('UnhandledPromiseRejectionWarning')
})
test('should exit when SIGINT is signalled', async () => {
const port = await findPort()
await testExitSignal(
'SIGINT',
['dev', next.testDir, '-p', String(port)],
/- Local:/
)
})
test('should exit when SIGTERM is signalled', async () => {
const port = await findPort()
await testExitSignal(
'SIGTERM',
['dev', next.testDir, '-p', String(port)],
/- Local:/
)
})
test('invalid directory', async () => {
const output = await next.runCommand(['dev', 'non-existent'])
expect(output.stderr).toContain(
'Invalid project directory provided, no such directory'
)
})
})
describe('export', () => {
test('--help', async () => {
const help = await next.runCommand(['export', '--help'])
expect(help.stderr).toMatch(
`error: unknown option '--help'\n(Did you mean --help?)`
)
expect(help.code).toBe(1)
})
test('run export command', async () => {
const help = await next.runCommand(['export'])
expect(help.stderr).toMatch(
`\`next export\` has been removed in favor of 'output: export' in next.config.js`
)
expect(help.code).toBe(1)
})
})
describe('info', () => {
function matchInfoOutput(stdout, { nextConfigOutput = '.*' } = {}) {
expect(stdout).toMatch(
new RegExp(`
Operating System:
Platform: .*
Arch: .*
Version: .*
Available memory \\(MB\\): .*
Available CPU cores: .*
Binaries:
Node: .*
npm: .*
Yarn: .*
pnpm: .*
Relevant Packages:
next: .*
eslint-config-next: .*
react: .*
react-dom: .*
typescript: .*${process.env.NEXT_RSPACK ? '\n next-rspack: .*' : ''}
Next.js Config:
output: ${nextConfigOutput}
`)
)
}
test('--help', async () => {
const help = await next.runCommand(['info', '--help'])
expect(help.stdout).toMatch(
/Prints relevant details about the current system which can be used to report/
)
})
test('-h', async () => {
const help = await next.runCommand(['info', '-h'])
expect(help.stdout).toMatch(
/Prints relevant details about the current system which can be used to report/
)
})
test('should print output', async () => {
const info = await next.runCommand(['info'])
expect((info.stderr || '').toLowerCase()).not.toContain('error')
matchInfoOutput(info.stdout)
})
test('should not leak package manager command errors', async () => {
const marker = 'next-info-yarn-command-not-found'
const binDir = await fs.mkdtemp(join(next.testDir, 'next-info-bin-'))
const pathKey =
Object.keys(process.env).find((key) => key.toLowerCase() === 'path') ??
'PATH'
const yarnPath = join(
binDir,
process.platform === 'win32' ? 'yarn.cmd' : 'yarn'
)
try {
await fs.writeFile(
yarnPath,
process.platform === 'win32'
? `@echo off\r\necho ${marker} 1>&2\r\nexit /b 1\r\n`
: `#!/bin/sh\necho ${marker} >&2\nexit 1\n`
)
if (process.platform !== 'win32') {
await fs.chmod(yarnPath, 0o755)
}
const info = await next.runCommand(['info'], {
env: {
[pathKey]: `${binDir}${path.delimiter}${process.env[pathKey] ?? ''}`,
// Match issue #97932's npm project so the fake Yarn only exercises
// binary version detection, not registry discovery.
npm_config_user_agent: 'npm',
},
})
expect(info.stdout).toContain('Yarn: N/A')
expect(info.stderr).not.toContain(marker)
} finally {
await fs.rm(binDir, { recursive: true, force: true })
}
})
test('should print output with next.config.mjs', async () => {
let info = { stdout: '', stderr: '' }
const originalPkg = await next.readFile('package.json')
try {
await next.patchFile(
'next.config.mjs',
`export default { output: 'standalone' }`
)
// Merge `type: 'module'` into the existing package.json so we keep
// the `packageManager` field that nextTestSetup generated. Without
// it corepack auto-fetches the latest pnpm, which is incompatible
// with the runner's Node version on CI.
await next.patchFile(
'package.json',
JSON.stringify({ ...JSON.parse(originalPkg), type: 'module' })
)
info = await next.runCommand(['info'], {
cwd: next.testDir,
})
} finally {
await next.deleteFile('next.config.mjs')
await next.patchFile('package.json', originalPkg)
}
expect((info.stderr || '').toLowerCase()).not.toContain('error')
matchInfoOutput(info.stdout, { nextConfigOutput: 'standalone' })
})
})
})
describe('CLI Usage: duplicate sass dependencies', () => {
const { next, isNextStart, skipped } = nextTestSetup({
files: join(__dirname, 'duplicate-sass'),
skipStart: true,
dependencies: reactDependencies,
skipDeployment: true,
})
if (skipped) return
// The original integration test relied on pre-existing fake `sass` and
// `node-sass` modules in `duplicate-sass/node_modules/`. In e2e mode the
// isolated `pnpm install` moves any pre-existing `node_modules` to
// `.ignored`, so we recreate the fake modules and reference them in
// `package.json` after install, before running `next dev`.
beforeAll(async () => {
if (!isNextStart) return
const pkg = await next.readJSON('package.json')
pkg.dependencies = {
...pkg.dependencies,
sass: '1.0.0',
'node-sass': '1.0.0',
}
await next.patchFile('package.json', JSON.stringify(pkg, null, 2))
for (const name of ['sass', 'node-sass']) {
await next.patchFile(
`node_modules/${name}/package.json`,
JSON.stringify({ name, version: '1.0.0' })
)
await next.patchFile(
`node_modules/${name}/index.js`,
'module.exports = {}\n'
)
}
})
;(isNextStart ? test : test.skip)('duplicate sass deps', async () => {
const port = await findPort()
let output = ''
let child: ChildProcess | undefined
const exit = next
.runCommand(['dev', next.testDir, '-p', String(port)], {
onStdout(msg) {
output += msg
},
onStderr(msg) {
output += msg
},
instance: (p) => {
child = p
},
})
.catch(() => {})
try {
await retry(() => {
expect(output).toMatch(/both `sass` and `node-sass` installed/)
})
} finally {
if (child) {
await killApp(child).catch(() => {})
}
await exit
}
})
})