Skip to content

Commit

Permalink
fix #3692: 0 now picks a random ephemeral port
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Feb 6, 2025
1 parent 11df6bf commit dc71977
Show file tree
Hide file tree
Showing 6 changed files with 42 additions and 13 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,14 @@

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

* Allow passing a port of 0 to the development server ([#3692](https://github.com/evanw/esbuild/issues/3692))

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.

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.

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.

* Minification now avoids inlining constants with direct `eval` ([#4055](https://github.com/evanw/esbuild/issues/4055))

Direct `eval` can be used to introduce a new variable like this:
Expand Down
8 changes: 7 additions & 1 deletion cmd/esbuild/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,13 @@ func (service *serviceType) handleIncomingPacket(bytes []byte) {
options.Host = value.(string)
}
if value, ok := request["port"]; ok {
options.Port = uint16(value.(int))
if value == 0 {
// 0 is the default value in Go, which we interpret as "try to
// pick port 8000". So Go uses -1 as the sentinel value instead.
options.Port = -1
} else {
options.Port = value.(int)
}
}
if value, ok := request["servedir"]; ok {
options.Servedir = value.(string)
Expand Down
5 changes: 4 additions & 1 deletion lib/shared/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ let mustBeRegExp = (value: RegExp | undefined): string | null =>
let mustBeInteger = (value: number | undefined): string | null =>
typeof value === 'number' && value === (value | 0) ? null : 'an integer'

let mustBeValidPortNumber = (value: number | undefined): string | null =>
typeof value === 'number' && value === (value | 0) && value >= 0 && value <= 0xFFFF ? null : 'a valid port number'

let mustBeFunction = (value: Function | undefined): string | null =>
typeof value === 'function' ? null : 'a function'

Expand Down Expand Up @@ -1091,7 +1094,7 @@ function buildOrContextImpl(
serve: (options = {}) => new Promise((resolve, reject) => {
if (!streamIn.hasFS) throw new Error(`Cannot use the "serve" API in this environment`)
const keys: OptionKeys = {}
const port = getFlag(options, keys, 'port', mustBeInteger)
const port = getFlag(options, keys, 'port', mustBeValidPortNumber)
const host = getFlag(options, keys, 'host', mustBeString)
const servedir = getFlag(options, keys, 'servedir', mustBeString)
const keyfile = getFlag(options, keys, 'keyfile', mustBeString)
Expand Down
2 changes: 1 addition & 1 deletion pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ func Transform(input string, options TransformOptions) TransformResult {

// Documentation: https://esbuild.github.io/api/#serve-arguments
type ServeOptions struct {
Port uint16
Port int
Host string
Servedir string
Keyfile string
Expand Down
6 changes: 5 additions & 1 deletion pkg/api/serve_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -773,7 +773,11 @@ func (ctx *internalContext) Serve(serveOptions ServeOptions) (ServeResult, error
}
if listener == nil {
// Otherwise pick the provided port
if result, err := net.Listen(network, net.JoinHostPort(host, fmt.Sprintf("%d", serveOptions.Port))); err != nil {
port := serveOptions.Port
if port < 0 || port > 0xFFFF {
port = 0 // Pick a random port if the provided port is out of range
}
if result, err := net.Listen(network, net.JoinHostPort(host, fmt.Sprintf("%d", port))); err != nil {
return ServeResult{}, err
} else {
listener = result
Expand Down
26 changes: 17 additions & 9 deletions pkg/cli/cli_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -1370,7 +1370,7 @@ func runImpl(osArgs []string, plugins []api.Plugin) int {

func parseServeOptionsImpl(osArgs []string) (api.ServeOptions, []string, error) {
host := ""
portText := "0"
portText := ""
servedir := ""
keyfile := ""
certfile := ""
Expand All @@ -1397,25 +1397,33 @@ func parseServeOptionsImpl(osArgs []string) (api.ServeOptions, []string, error)
}

// Specifying the host is optional
var err error
if strings.ContainsRune(portText, ':') {
var err error
host, portText, err = net.SplitHostPort(portText)
if err != nil {
return api.ServeOptions{}, nil, err
}
}

// Parse the port
port, err := strconv.ParseInt(portText, 10, 32)
if err != nil {
return api.ServeOptions{}, nil, err
}
if port < 0 || port > 0xFFFF {
return api.ServeOptions{}, nil, fmt.Errorf("Invalid port number: %s", portText)
var port int64
if portText != "" {
port, err = strconv.ParseInt(portText, 10, 32)
if err != nil {
return api.ServeOptions{}, nil, err
}
if port < 0 || port > 0xFFFF {
return api.ServeOptions{}, nil, fmt.Errorf("Invalid port number: %s", portText)
}
if port == 0 {
// 0 is the default value in Go, which we interpret as "try to
// pick port 8000". So Go uses -1 as the sentinel value instead.
port = -1
}
}

return api.ServeOptions{
Port: uint16(port),
Port: int(port),
Host: host,
Servedir: servedir,
Keyfile: keyfile,
Expand Down

0 comments on commit dc71977

Please sign in to comment.