Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/runtime/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ Bun reads these environment variables to configure aspects of its behavior.
| `FORCE_COLOR` | If `FORCE_COLOR=1`, then ANSI color output is forced on, even if `NO_COLOR` is set. |
| `BUN_CONFIG_MAX_HTTP_REQUESTS` | Sets the maximum number of concurrent HTTP requests sent by fetch and `bun install`. Defaults to `256`. Lower it if you run into rate limits or connection issues. |
| `BUN_CONFIG_NO_CLEAR_TERMINAL_ON_RELOAD` | If `BUN_CONFIG_NO_CLEAR_TERMINAL_ON_RELOAD=true`, then `bun --watch` does not clear the console on reload |
| `BUN_WATCHER_USE_POLLING` | If `BUN_WATCHER_USE_POLLING=1`, `--watch` and `--hot` poll the watched files with `stat()` instead of native filesystem events. Use it on Docker bind mounts, WSL `/mnt/*` paths, and network filesystems, where native events do not arrive. `0` keeps the native watcher. See [watch mode](/runtime/watch-mode#docker-wsl-and-network-filesystems). |
| `BUN_WATCHER_POLL_INTERVAL` | The interval, in milliseconds, between polls when `--watch` or `--hot` polls. Defaults to `100`. |
| `DO_NOT_TRACK` | Disable uploading crash reports to `bun.report` on crash. On macOS & Windows, crash report uploads are enabled by default. Bun sends no other telemetry, though we plan to add some. If `DO_NOT_TRACK=1`, then auto-uploading crash reports and telemetry are both [disabled](https://do-not-track.dev/). |
| `BUN_OPTIONS` | Prepends command-line arguments to any Bun execution. For example, `BUN_OPTIONS="--hot"` makes `bun run dev` behave like `bun --hot run dev`. |

Expand Down
20 changes: 20 additions & 0 deletions docs/runtime/watch-mode.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,26 @@ bun --watch test
signal, e.g. `bun --watch --watch-kill-signal SIGINT index.ts`.
</Note>

### Docker, WSL, and network filesystems

The native filesystem watcher APIs do not report a change that comes from the other side of a mount. The watch succeeds, and then no event arrives. This happens on:

- Docker bind mounts of a Windows or macOS host directory
- WSL paths under `/mnt/c`
- NFS and SMB shares, and VM shared folders

For these, `--watch` and `--hot` can poll: Bun calls `stat()` on each watched file and directory on an interval and reloads when the modification time, size, or inode of a file changes. On Linux, Bun turns polling on by itself when the directory you run it in is on a 9p, NFS, SMB, or Parallels filesystem, and prints a note. WSL `/mnt/*` paths are 9p, for example. For any other setup, set `BUN_WATCHER_USE_POLLING=1`:

```bash terminal icon="terminal"
BUN_WATCHER_USE_POLLING=1 bun --watch index.ts
```

In a container, set it once with `ENV` in the Dockerfile or under `environment:` in the Compose file.

`BUN_WATCHER_POLL_INTERVAL` sets the interval in milliseconds. The default is `100`. Polling uses more CPU than native events. `BUN_WATCHER_USE_POLLING=0` keeps the native watcher even where Bun would turn polling on.

A watched directory changes when an entry is added, removed, or renamed. The dev server uses that to retry an import that failed, so it recovers when you create the missing file.

---

## `--hot` mode
Expand Down
3 changes: 3 additions & 0 deletions src/bun_core/env_var.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ platform_specific_new!(pub LIBRARY_PATH: string, posix = "LIBRARY_PATH", windows
new!(pub BUN_TEST_DRAIN_EVENT_LOOP: boolean, "BUN_TEST_DRAIN_EVENT_LOOP", { default: false });
new!(pub BUN_TMPDIR: string, "BUN_TMPDIR", {});
new!(pub BUN_WATCHER_TRACE: string, "BUN_WATCHER_TRACE", {});
// No default: unset lets `Watcher::init` pick polling from the filesystem type.
new!(pub BUN_WATCHER_USE_POLLING: boolean, "BUN_WATCHER_USE_POLLING", {});
new!(pub BUN_WATCHER_POLL_INTERVAL: unsigned, "BUN_WATCHER_POLL_INTERVAL", {});
new!(pub CI: boolean, "CI", {});
new!(pub CI_COMMIT_SHA: string, "CI_COMMIT_SHA", {});
new!(pub CI_JOB_URL: string, "CI_JOB_URL", {});
Expand Down
5 changes: 3 additions & 2 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4698,10 +4698,11 @@ pub mod bv2_impl {
break 'add_watchers;
}

let bun_watcher = this.bun_watcher_mut().unwrap();
// TODO: support explicit watchFiles array. this is not done
// right now because DevServer requires a table to map
// watched files and dirs to their respective dependants.
let fd = if bun_watcher::REQUIRES_FILE_DESCRIPTORS {
let fd = if bun_watcher.requires_file_descriptors() {
let mut buf = bun_paths::path_buffer_pool::get();
// On kqueue platforms paths are already
// posix-separated so `z()` alone suffices.
Expand All @@ -4719,7 +4720,7 @@ pub mod bv2_impl {

// Failures to watch are intentionally ignored.
if !matches!(
this.bun_watcher_mut().unwrap().add_file::<true>(
bun_watcher.add_file::<true>(
fd,
&load.path,
bun_wyhash::hash(load.path.as_ref()) as u32,
Expand Down
8 changes: 8 additions & 0 deletions src/jsc/hot_reloader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ pub enum ImportWatcher {
}

impl ImportWatcher {
#[inline]
pub fn requires_file_descriptors(&self) -> bool {
match self {
ImportWatcher::Hot(w) | ImportWatcher::Watch(w) => w.requires_file_descriptors(),
ImportWatcher::None => false,
}
}

/// Look up the `package_json` column for `hash` under the watcher's
/// mutex.
///
Expand Down
5 changes: 3 additions & 2 deletions src/runtime/bake/dev_server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1369,7 +1369,8 @@ impl DirectoryWatchStore {
Ok(None) | Err(_) => None,
};

let (fd, owned_fd): (bun_sys::Fd, bool) = if bun_watcher::REQUIRES_FILE_DESCRIPTORS {
let requires_fds = self.dev_bun_watcher().requires_file_descriptors();
let (fd, owned_fd): (bun_sys::Fd, bool) = if requires_fds {
if let Some(fd) = cache_fd {
(fd, false)
} else {
Expand Down Expand Up @@ -1402,7 +1403,7 @@ impl DirectoryWatchStore {
(bun_sys::Fd::INVALID, false)
};
let fd_guard = scopeguard::guard(fd, move |fd| {
if bun_watcher::REQUIRES_FILE_DESCRIPTORS && owned_fd {
if requires_fds && owned_fd {
fd.close();
}
});
Expand Down
14 changes: 7 additions & 7 deletions src/runtime/jsc_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3410,9 +3410,14 @@ fn transpile_source_code_inner(
{
break 'auto_watch;
}
// SAFETY: `bun_watcher` is the `*mut ImportWatcher`
// set when `is_watcher_enabled()`; cast recovers the concrete
// type.
let watcher =
unsafe { &mut *(*jsc_vm).bun_watcher.cast::<bun_jsc::ImportWatcher>() };
// kqueue watchers need a file descriptor to receive event
// notifications on it; inotify/win32 watch by path.
let input_fd = if bun_watcher::REQUIRES_FILE_DESCRIPTORS {
// notifications on it; inotify/win32/polling watch by path.
let input_fd = if watcher.requires_file_descriptors() {
let mut buf = bun_paths::path_buffer_pool::get();
if path.text.len() >= buf.len() {
break 'auto_watch;
Expand All @@ -3426,11 +3431,6 @@ fn transpile_source_code_inner(
bun_sys::Fd::INVALID
};
let hash = bun_watcher::Watcher::get_hash(path.text);
// SAFETY: `bun_watcher` is the `*mut ImportWatcher`
// set when `is_watcher_enabled()`; cast recovers the concrete
// type.
let watcher =
unsafe { &mut *(*jsc_vm).bun_watcher.cast::<bun_jsc::ImportWatcher>() };
let added =
watcher.add_file::<true>(input_fd, path.text, hash, bun_sys::Fd::INVALID, None);
if !matches!(added, Ok(bun_watcher::FdOwnership::Watcher)) {
Expand Down
2 changes: 1 addition & 1 deletion src/watcher/INotifyWatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ pub(crate) fn watch_loop_cycle(this: &mut Watcher) -> bun_sys::Result<()> {
use crate::watcher_impl::WatchItemColumns;
let _flush = Output::flush_guard();

let events = this.platform.read()?;
let events = this.platform.native_mut().read()?;
if events.is_empty() {
return Ok(());
}
Expand Down
2 changes: 1 addition & 1 deletion src/watcher/KEventWatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ fn watch_event_from_kevent(kevent: &libc::kevent) -> WatchEvent {

pub(crate) fn watch_loop_cycle(this: &mut Watcher) -> bun_sys::Result<()> {
let _flush = Output::flush_guard();
let fd = this.platform.fd;
let fd = this.platform.native_mut().fd;

let mut changelist: [libc::kevent; CHANGELIST_COUNT] = bun_core::ffi::zeroed();

Expand Down
Loading
Loading