Skip to content

Commit

Permalink
fix #21: implement a basic polling watch mode
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jan 31, 2021
1 parent cd1cce3 commit 94f89b8
Show file tree
Hide file tree
Showing 15 changed files with 861 additions and 42 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## Unreleased

* Implement a simple cross-platform watch mode ([#21](https://github.com/evanw/esbuild/issues/21))

With this release, you can use the `--watch` flag to run esbuild in watch mode which watches the file system for changes and does an incremental build when something has changed. The watch mode implementation uses polling instead of OS-specific file system events for portability.

Note that it is still possible to implement watch mode yourself using esbuild's incremental build API and a file watcher library of your choice if you don't want to use a polling-based approach. Also note that this watch mode feature is about improving developer convenience and does not have any effect on incremental build time (i.e. watch mode is not faster than other forms of incremental builds).

The new polling system is indended to use relatively little CPU vs. a traditional polling system that scans the whole directory tree at once. The file system is still scanned regularly but each scan only checks a random subset of your files to reduce CPU usage. This means a change to a file will be picked up soon after the change is made but not necessarily instantly. With the current heuristics, large projects should be completely scanned around every 2 seconds so in the worst case it could take up to 2 seconds for a change to be noticed. However, after a change has been noticed the change's path goes on a short list of recently changed paths which are checked on every scan, so further changes to recently changed files should be noticed almost instantly.

* Add `pluginData` to pass data between plugins ([#696](https://github.com/evanw/esbuild/issues/696))

You can now return additional data from a plugin in the optional `pluginData` field and it will be passed to the next plugin that runs in the plugin chain. So if you return it from an `onLoad` plugin, it will be passed to the `onResolve` plugins for any imports in that file, and if you return it from an `onResolve` plugin, an arbitrary one will be passed to the `onLoad` plugin when it loads the file (it's arbitrary since the relationship is many-to-one). This is useful to pass data between different plugins without them having to coordinate directly.
Expand Down
1 change: 1 addition & 0 deletions cmd/esbuild/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ var helpText = func(colors logger.Colors) string {
--summary Print some helpful information at the end of a build
--target=... Environment target (e.g. es2017, chrome58, firefox57,
safari11, edge16, node10, default esnext)
--watch Watch mode: rebuild on file system changes
` + colors.Bold + `Advanced options:` + colors.Default + `
--banner=... Text to be prepended to each output file
Expand Down
68 changes: 67 additions & 1 deletion cmd/esbuild/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,18 @@ import (

type responseCallback = func(interface{})
type rebuildCallback = func(uint32) []byte
type watchStopCallback = func()
type serverStopCallback = func()

type serviceType struct {
mutex sync.Mutex
callbacks map[uint32]responseCallback
rebuilds map[int]rebuildCallback
watchStops map[int]watchStopCallback
serveStops map[int]serverStopCallback
nextID uint32
nextRebuildID int
nextWatchID int
outgoingPackets chan outgoingPacket
}

Expand All @@ -46,6 +49,7 @@ func runService() {
service := serviceType{
callbacks: make(map[uint32]responseCallback),
rebuilds: make(map[int]rebuildCallback),
watchStops: make(map[int]watchStopCallback),
serveStops: make(map[int]serverStopCallback),
outgoingPackets: make(chan outgoingPacket),
}
Expand Down Expand Up @@ -194,6 +198,32 @@ func (service *serviceType) handleIncomingPacket(bytes []byte) (result outgoingP
bytes: rebuild(p.id),
}

case "watch-stop":
watchID := request["watchID"].(int)
refCount := 0
watchStop := func() watchStopCallback {
// Only mutate the map while inside a mutex
service.mutex.Lock()
defer service.mutex.Unlock()
if watchStop, ok := service.watchStops[watchID]; ok {
// This watch is now considered finished. This matches the +1 reference
// count at the return of the serve call.
refCount = -1
return watchStop
}
return nil
}()
if watchStop != nil {
watchStop()
}
return outgoingPacket{
bytes: encodePacket(packet{
id: p.id,
value: make(map[string]interface{}),
}),
refCount: refCount,
}

case "serve-stop":
serveID := request["serveID"].(int)
refCount := 0
Expand Down Expand Up @@ -276,6 +306,10 @@ func (service *serviceType) handleIncomingPacket(bytes []byte) (result outgoingP
return callback
}()

if callback == nil {
panic(fmt.Sprintf("callback nil for id %d, value %v", p.id, p.value))
}

callback(p.value)
return outgoingPacket{}
}
Expand All @@ -293,6 +327,7 @@ func (service *serviceType) handleBuildRequest(id uint32, request map[string]int
key := request["key"].(int)
write := request["write"].(bool)
incremental := request["incremental"].(bool)
hasOnRebuild := request["hasOnRebuild"].(bool)
serveObj, isServe := request["serve"].(interface{})
flags := decodeStringArray(request["flags"].([]interface{}))

Expand Down Expand Up @@ -339,9 +374,13 @@ func (service *serviceType) handleBuildRequest(id uint32, request map[string]int
}

rebuildID := service.nextRebuildID
watchID := service.nextWatchID
if incremental {
service.nextRebuildID++
}
if options.Watch != nil {
service.nextWatchID++
}

resultToResponse := func(result api.BuildResult) map[string]interface{} {
response := map[string]interface{}{
Expand All @@ -355,9 +394,22 @@ func (service *serviceType) handleBuildRequest(id uint32, request map[string]int
if incremental {
response["rebuildID"] = rebuildID
}
if options.Watch != nil {
response["watchID"] = watchID
}
return response
}

if options.Watch != nil && hasOnRebuild {
options.Watch.OnRebuild = func(result api.BuildResult) {
service.sendRequest(map[string]interface{}{
"command": "watch-rebuild",
"watchID": watchID,
"args": resultToResponse(result),
})
}
}

options.Write = write
options.Incremental = incremental
result := api.Build(options)
Expand All @@ -380,7 +432,21 @@ func (service *serviceType) handleBuildRequest(id uint32, request map[string]int
}()

// Make sure the build doesn't finish until "dispose" has been called
refCount = 1
refCount++
}

if options.Watch != nil {
func() {
// Only mutate the map while inside a mutex
service.mutex.Lock()
defer service.mutex.Unlock()
service.watchStops[watchID] = func() {
result.Stop()
}
}()

// Make sure the build doesn't finish until "stop" has been called
refCount++
}

return outgoingPacket{
Expand Down
9 changes: 9 additions & 0 deletions internal/fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ type FS interface {

// This is used in the implementation of "Entry"
kind(dir string, base string) (symlink string, kind EntryKind)

// This is a set of all files used and all directories checked. The build
// must be invalidated if any of these watched files change.
WatchData() WatchData
}

type WatchData struct {
// These functions return true if the file system entry has been modified
Paths map[string]func() bool
}

type ModKey struct {
Expand Down
7 changes: 5 additions & 2 deletions internal/fs/fs_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ func (*mockFS) Rel(base string, target string) (string, bool) {
}

func (fs *mockFS) kind(dir string, base string) (symlink string, kind EntryKind) {
// This will never be called
return
panic("This should never be called")
}

func (fs *mockFS) WatchData() WatchData {
panic("This should never be called")
}
Loading

0 comments on commit 94f89b8

Please sign in to comment.