Skip to content

Commit dc71977

Browse files
committedFeb 6, 2025
fix #3692: 0 now picks a random ephemeral port
1 parent 11df6bf commit dc71977

File tree

6 files changed

+42
-13
lines changed

6 files changed

+42
-13
lines changed
 

‎CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,14 @@
184184

185185
This PR was contributed by [@MikeWillCook](https://github.com/MikeWillCook).
186186

187+
* Allow passing a port of 0 to the development server ([#3692](https://github.com/evanw/esbuild/issues/3692))
188+
189+
Unix sockets interpret a port of 0 to mean "pick a random unused port in the [ephemeral port](https://en.wikipedia.org/wiki/Ephemeral_port) range". However, esbuild's default behavior when the port is not specified is to pick the first unused port starting from 8000 and upward. This is more convenient because port 8000 is typically free, so you can for example restart the development server and reload your app in the browser without needing to change the port in the URL. Since esbuild is written in Go (which does not have optional fields like JavaScript), not specifying the port in Go means it defaults to 0, so previously passing a port of 0 to esbuild caused port 8000 to be picked.
190+
191+
Starting with this release, passing a port of 0 to esbuild when using the CLI or the JS API will now pass port 0 to the OS, which will pick a random ephemeral port. To make this possible, the `Port` option in the Go API has been changed from `uint16` to `int` (to allow for additional sentinel values) and passing a port of -1 in Go now picks a random port. Both the CLI and JS APIs now remap an explicitly-provided port of 0 into -1 for the internal Go API.
192+
193+
Another option would have been to change `Port` in Go from `uint16` to `*uint16` (Go's closest equivalent of `number | undefined`). However, that would make the common case of providing an explicit port in Go very awkward as Go doesn't support taking the address of integer constants. This tradeoff isn't worth it as picking a random ephemeral port is a rare use case. So the CLI and JS APIs should now match standard Unix behavior when the port is 0, but you need to use -1 instead with Go API.
194+
187195
* Minification now avoids inlining constants with direct `eval` ([#4055](https://github.com/evanw/esbuild/issues/4055))
188196

189197
Direct `eval` can be used to introduce a new variable like this:

‎cmd/esbuild/service.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,13 @@ func (service *serviceType) handleIncomingPacket(bytes []byte) {
372372
options.Host = value.(string)
373373
}
374374
if value, ok := request["port"]; ok {
375-
options.Port = uint16(value.(int))
375+
if value == 0 {
376+
// 0 is the default value in Go, which we interpret as "try to
377+
// pick port 8000". So Go uses -1 as the sentinel value instead.
378+
options.Port = -1
379+
} else {
380+
options.Port = value.(int)
381+
}
376382
}
377383
if value, ok := request["servedir"]; ok {
378384
options.Servedir = value.(string)

‎lib/shared/common.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ let mustBeRegExp = (value: RegExp | undefined): string | null =>
2828
let mustBeInteger = (value: number | undefined): string | null =>
2929
typeof value === 'number' && value === (value | 0) ? null : 'an integer'
3030

31+
let mustBeValidPortNumber = (value: number | undefined): string | null =>
32+
typeof value === 'number' && value === (value | 0) && value >= 0 && value <= 0xFFFF ? null : 'a valid port number'
33+
3134
let mustBeFunction = (value: Function | undefined): string | null =>
3235
typeof value === 'function' ? null : 'a function'
3336

@@ -1091,7 +1094,7 @@ function buildOrContextImpl(
10911094
serve: (options = {}) => new Promise((resolve, reject) => {
10921095
if (!streamIn.hasFS) throw new Error(`Cannot use the "serve" API in this environment`)
10931096
const keys: OptionKeys = {}
1094-
const port = getFlag(options, keys, 'port', mustBeInteger)
1097+
const port = getFlag(options, keys, 'port', mustBeValidPortNumber)
10951098
const host = getFlag(options, keys, 'host', mustBeString)
10961099
const servedir = getFlag(options, keys, 'servedir', mustBeString)
10971100
const keyfile = getFlag(options, keys, 'keyfile', mustBeString)

‎pkg/api/api.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,7 @@ func Transform(input string, options TransformOptions) TransformResult {
472472

473473
// Documentation: https://esbuild.github.io/api/#serve-arguments
474474
type ServeOptions struct {
475-
Port uint16
475+
Port int
476476
Host string
477477
Servedir string
478478
Keyfile string

‎pkg/api/serve_other.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -773,7 +773,11 @@ func (ctx *internalContext) Serve(serveOptions ServeOptions) (ServeResult, error
773773
}
774774
if listener == nil {
775775
// Otherwise pick the provided port
776-
if result, err := net.Listen(network, net.JoinHostPort(host, fmt.Sprintf("%d", serveOptions.Port))); err != nil {
776+
port := serveOptions.Port
777+
if port < 0 || port > 0xFFFF {
778+
port = 0 // Pick a random port if the provided port is out of range
779+
}
780+
if result, err := net.Listen(network, net.JoinHostPort(host, fmt.Sprintf("%d", port))); err != nil {
777781
return ServeResult{}, err
778782
} else {
779783
listener = result

‎pkg/cli/cli_impl.go

+17-9
Original file line numberDiff line numberDiff line change
@@ -1370,7 +1370,7 @@ func runImpl(osArgs []string, plugins []api.Plugin) int {
13701370

13711371
func parseServeOptionsImpl(osArgs []string) (api.ServeOptions, []string, error) {
13721372
host := ""
1373-
portText := "0"
1373+
portText := ""
13741374
servedir := ""
13751375
keyfile := ""
13761376
certfile := ""
@@ -1397,25 +1397,33 @@ func parseServeOptionsImpl(osArgs []string) (api.ServeOptions, []string, error)
13971397
}
13981398

13991399
// Specifying the host is optional
1400+
var err error
14001401
if strings.ContainsRune(portText, ':') {
1401-
var err error
14021402
host, portText, err = net.SplitHostPort(portText)
14031403
if err != nil {
14041404
return api.ServeOptions{}, nil, err
14051405
}
14061406
}
14071407

14081408
// Parse the port
1409-
port, err := strconv.ParseInt(portText, 10, 32)
1410-
if err != nil {
1411-
return api.ServeOptions{}, nil, err
1412-
}
1413-
if port < 0 || port > 0xFFFF {
1414-
return api.ServeOptions{}, nil, fmt.Errorf("Invalid port number: %s", portText)
1409+
var port int64
1410+
if portText != "" {
1411+
port, err = strconv.ParseInt(portText, 10, 32)
1412+
if err != nil {
1413+
return api.ServeOptions{}, nil, err
1414+
}
1415+
if port < 0 || port > 0xFFFF {
1416+
return api.ServeOptions{}, nil, fmt.Errorf("Invalid port number: %s", portText)
1417+
}
1418+
if port == 0 {
1419+
// 0 is the default value in Go, which we interpret as "try to
1420+
// pick port 8000". So Go uses -1 as the sentinel value instead.
1421+
port = -1
1422+
}
14151423
}
14161424

14171425
return api.ServeOptions{
1418-
Port: uint16(port),
1426+
Port: int(port),
14191427
Host: host,
14201428
Servedir: servedir,
14211429
Keyfile: keyfile,

0 commit comments

Comments
 (0)