Repository navigation
Conversation
…ct's own package folder A path with a node_modules component is a package file, except for a project that is itself a package folder of node_modules: each of its own paths has that component. NodeModulesScope::below_root(start) finds the package folder that holds the start folder (through node_modules/<name> or node_modules/@scope/<name>). At or below that folder only the components after it count. Every other path is tested whole, and so is every path when the start folder is node_modules itself, a scope folder or a dot folder.
The watcher took every path with a node_modules component for a package file. A project that runs from node_modules/<pkg> has that component in each of its own paths, so --watch, --hot, bun test --watch and --check watched none of its imports, and a folder of the project was never read again after a failed lookup. Watcher::init now takes the rule of its owner and turns it, once, into a NodeModulesScope. ImportWatcher::add_file, the file-loader arm, Watcher::is_eligible_directory and check_command::watching read that one scope. The runtime and bun build --watch count node_modules from the package folder that holds the folder bun started in. The dev server keeps the rule of the whole path, as its bundler does.
Collaborator
Author
|
Updated 3:58 PM PT - Oct 25th, 2026
✅ @robobun, your commit 0bc550c9bb75f687edddf38b9f27b93afeebfe6d passed in 🧪 To try this PR locally: bunx bun-pr 46910That installs a local version of the PR into your bun-46910 --bun |
Collaborator
Author
|
Status: draft. It waits for #46809, which it is stacked on, and for #46909, which has its How it was reproduced, on bun 1.4.3-canary.1 (367d939), on main 179664e and on #46809 (9c6c8ee), Linux x64:
With this change step 4 prints Pull request: #46910 |
NodeModulesScope asked resolve_path::is_parent_or_equal whether a path is in the project folder. That helper folds ASCII case on every system but Linux and takes a backslash for a separator everywhere, so on a case-sensitive macOS volume a sibling package that differs by case counted as the project. The scope now compares the folder itself: byte for byte on POSIX, and on Windows with either separator and either case of a letter.
… case for a start below it The Windows watcher takes no file outside the folder bun starts in, for any project. The --watch --check case started in my-pkg/src and waited for a change of ../lib/imported.ts, so it could not pass there.
Collaborator
Author
|
The base branch of this PR gained a commit: For the rebase of this PR:
|
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft, stacked on #46809. Its
bun_pathscommits are older than #46909.Problem
node_modules/<pkg>gets no reload for its imports under--watchand--hot, andbun test --watchruns nothing again.node_modulescomponent for a package path (ImportWatcher::add_file,Watcher::is_eligible_directory,check_command::watching).Fix
Watcher::inittakes the rule of its owner and builds oneNodeModulesScope(bun test --coverage: count the files of a project that is a package folder ofnode_modules#46909) for the gates.bun build --watchcountnode_modulesfrom the package folder that holds the start folder. The dev server keeps the whole-path rule, as its bundler does.test/cli/watch/watch.test.ts, 11 new cases fail on watcher: skip packages by anode_modulespath component, not by the letters #46809. Self-reviewed: 29 concerns, 26 addressed.Background
Watcherhas three owners: the runtime,bun build --watchand the dev server.(root, path)test at each gate: it finds the package folder again per hit.Downsides
bun ./index.html(bun run ignore HTML files when they have anode_modulesfolder in path #24953),bun run --aot --watch, a Windows folder entered through a link (watcher: accept the real path spelling of the project root on Windows #39832).process.chdir(), a--watchrestart counts from the new folder (--watchdoesn't work afterprocess.chdir()is run #19163, watch: restart from the directory bun was started in #41799).Notes
Why a draft. #46809 has an open thread on
ImportWatcher::add_file: decide a package file by the spelling of its path, or carry the verdict from the resolver. This change builds on the first answer. #45594 and #42749 add other entry points toImportWatcherwith a whole-path test of their own. They need to asknode_modules_scope()for this change to hold after they land.#46809 has moved since this draft was pushed. It now also takes a path component named
node_modules.nosyncfor a package store (ec504f6), and it has a merge of main. This draft is older than both and conflicts with them. Its rebase follows when #46809 lands:NodeModulesScopemust then match that name too.#46909 has moved too. The rebase has these steps from it:
NodeModulesScopehas a lifetime parameter now. The field ofWatcherand the return type ofVirtualMachine::watcher_for_threadsbecomeNodeModulesScope<'static>.bun run --aot --watch(WatchedFilesinsrc/runtime/cli/run_compiled.rs). It callsWatcher::initwith two arguments, andadd_read_file(line 1320) has the whole-path test thatcheck_command::watchinghad. Innode_modules/my-pkg, an edit to an import does not restart the program there. It must asknode_modules_scope()too.Repro. bun 1.4.3-canary.1 (367d939), main 179664e and #46809 (9c6c8ee), Linux x64.
An edit to
index.jsreloads: the entry is added by path after the first run, with no rule. The same project int/packages/my-pkgreloads.node --watch26.3.0 andtsx watch4.23.15 reload in this layout,nodemon3.1.14 does not.The rule is
NodeModulesScopeof #46909, built once inWatcher::initfrom the folder bun started in. The project folder is compared byte for byte on POSIX. On Windows either separator and either case of a letter match.node_modulescomponentnode_modules,node_modules/@scope,node_modules/.storenode_modules/my-pkg,node_modules/my-pkg/srcnode_modules/my-pkgnode_modules/.bun/my-pkg@1.0.0/node_modules/my-pkgnode_modules/.store/<id>/packageWhere the rule is read.
ImportWatcher::add_file: the module loader offers each file it reads. It declines a package file before the watcher mutex.jsc_hooks.rs: it asks first, because on kqueue it opens a descriptor beforeadd_file.Watcher::is_eligible_directory: the folder of each added file, and each folder in which a resolver lookup failed.bun build --watchgets it through the sameWatcher::initcall.check_command::watching:VirtualMachine::watcher_for_threadshands the scope out with its closure. A path with/node_modules/in a start folder outside a package folder returns with no join, as before.bun test --changed, the dev server's own directory watches.bun_pathsloses the two root-less predicates of watcher: skip packages by anode_modulespath component, not by the letters #46809. Their three callers ask the scope.What the tests pin (
watch.test.ts, block "a project in a package folder of node_modules"). A negative claim is read fromBUN_WATCHER_TRACEor from open descriptors, never from a timeout.--watchinnode_modules/my-pkg,--hotinnode_modules/@scope/my-pkgand innode_modules/.store/<id>/package: an edit to an import reloads. A nested package, a hoisted package, a file added tonode_modulesand the sibling foldermy-pkg-2see no event.node_modules,node_modules/@scopeandnode_modules/.store: only the entry is watched.--hot: a file of the file loader, a module loaded byrequire, a module saved by rename.--hotafterprocess.chdir(): a program that moves into a package folder does not get that package watched, and a project that moves out of its package folder stays watched.bun test --watchin the physical folder of the isolated linker: an edit to an import and an edit to the test file.--watch --check: a fix inlib/imported.ts, which only the type checker has read, is checked again, and the declarations of a package see no event.--watch, started inmy-pkg/src: an edit to../lib/util.jsreloads. Not on Windows: that watcher takes no file outside the folder bun starts in, for any project (Bun doesnt pick changes in monorepos packages #9547).bun build --watch: a save by rename rebuilds, and the folder of the project reports it.--hot: a file that is added to a folder in which a lookup failed is found on the next reload.Measurements, Linux x64, base #46809 (9c6c8ee):
node_modulespath component, not by the letters #46809 (perf_event_open, 1e6 calls, 3 identical runs, scalarmemmemstub of the unit tests, so differences only). Start outside a package folder: project file 1,114 to 1,117 instructions, package file 731 to 737, project folder 804 to 807, package folder 731 to 737. Branches are equal: 280, 185, 200, 186. Start innode_modules/my-pkg: own file 843, nested package 900, hoisted package 771, the project folder 789.node_modulesscans, gdb tally onhighway_memmem, debug builds: 4,003 for 2,000 project modules and the entry under--watch(base 4,002), 2,003 for 2,000 package modules (base 2,002), 0 with no watch flag (base 0). The one more is the scan of the start folder inWatcher::init.Watcher::initwith 2,001 modules, 1 with--watch --check, 1 for--hotwith a dev server (0 for the watcher of the dev server).size_of::<Watcher>(): 35,936 to 35,952 bytes.size): text 107,557,956 to 107,560,004 (+2,048), data +0, bss +0.--watch2,003 / 2,009,--hot2,003 / 2,009,test --watch5 / 13,build --watch7 / 13, dev server 7 / 18.node_modules/my-pkg(the same innode_modules/@scope/my-pkg):--watchand--hot1 to 3 watches and 6 to 9 descriptors,test --watch0 to 3 and 8 to 11,build --watch2 to 3 and 8 to 9. No watched path is under<cwd>/node_modulesor above the package.node_modules/my-pkg/srcwith an import of../lib/util.js: 1 to 3 watches, 6 to 9 descriptors.node_modules,node_modules/@sandnode_modules/.storewith a 192-module package: 1 watch and 6 descriptors, equal to base.node_modules/my-pkg: 0 inotify watches and 11 descriptors on base and here.--watch --checkwith 161 files read, start outside a package folder: 4 joins and 4 watch calls (base 4 and 4, one more of each in both builds when the checker readspackage.jsontwice). Innode_modules/my-pkg: 161 joins and 4 watch calls (base 0 and 0).perf,valgrind,straceandbloatyare not installed here.Not tested.
bun bakein this layout: its VM watcher takes the new rule, the watcher of its dev server does not. macOS and Windows were run by CI only.bun run rust:check-allpasses forx86_64-pc-windows-msvc,aarch64-apple-darwinandx86_64-unknown-freebsd.Other places that take such a project for a package are listed in #46909: the dev server's bundler,
bun ./index.html, tsconfigextends, the extension order of the resolver, macros.Open pull requests on the same lines. #35596 replaces only the Windows arms of
Watcher.rsand does not overlap. #39832 adds a second spelling of the root on Windows, which the scope does not have yet.Self-review, the 3 concerns that are not addressed.
watch.test.ts.Suites run on a debug build.
test/cli/watch/watch.test.ts(57 pass, 1 skip),watcher-trace.test.ts,test/cli/hot/watch.test.ts,watch-many-dirs.test.ts,hot.test.ts,test/cli/test/test-changed.test.ts,test/bake/dev/html.test.ts,test/bake/dev/hot.test.ts, the source lintswatcher-node-modules-needleandbyte-search,cargo test -p bun_paths,cargo clippyfor the four crates.no test proof · iteration 1 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/watch/watch.test.ts