Skip to content

Do not read the directory of a sideEffects glob or a --filter path as glob syntax - #44939

Open
robobun wants to merge 9 commits into
mainfrom
robobun/a4abc1e0/glob-literal-base
Open

robobun wants to merge 9 commits into
mainfrom
robobun/a4abc1e0/glob-literal-base

Conversation

@robobun

@robobun robobun commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • Under [client]/app or {old,new}/app, bun build drops a file that a "sideEffects" glob lists, at exit 0. --filter ./packages/a prints error: No workspace packages matched the filter "./packages/a". --filter '!./packages/a' excludes nothing.
  • c++(old)/app fails since 1.4.4: Add bun lint and bun format #44805 made +(old) a group in every pattern.
  • PackageJSON::parse (src/resolver/package_json.rs) and workspace_selection::parse (src/install/PackageManager/workspace_selection.rs) join a directory in front of the pattern. They match the whole string as a glob, so [client] is a character class.

Fix

  • bun_glob::literal_base_len finds where the joined string stops being the directory. Callers compare that part as bytes and match only the rest.
  • A path selector that spells a workspace directory selects it, as in pnpm.
  • Verified: dce.test.ts, filter-workspace.test.ts, bun-add-filter.test.ts. 64 of 67 new cases fail on main. Plain directories: no change (Notes).
  • Self-reviewed: 11 concerns raised, 11 addressed.

Background

  • "sideEffects" lists the files a bundler must keep when nothing uses their exports.
  • A --filter that starts with . or is {dir} selects by directory.
  • Weighed: escaping the directory (it stays pattern text) and package-relative entries (changes exact and ../ entries).

Downsides

Notes

The rule. The joined string is cut at the longest prefix that it shares with the directory, at a path component boundary of both. So an entry with .. has a shorter directory part (../shared/ind*.js is under the parent of the package), and text that goes back into the directory it left is a name again (../[x] typed inside packages/[x]). What is left after the cut is empty or starts with a separator, so it reads as it did inside the joined string.

Who is affected. A project whose path, or a workspace or package directory in it, has a name with a closed bracket or brace group ([client], {old,new}, {1..3}). Since 1.4.4 also a name with a group: c++(old), app@(v2), +(app), *(x), ?(x). On main the matcher already reads an unclosed [ or { as itself. Only a linked bundle loses the file (bun build, Bun.build, production HTML routes). Running the source keeps it. For --filter, a positive path selector fails loud. A negated one fails silent: bun add and bun remove then edit the workspace that the selector names. I searched the tracker for bracket and brace directories with sideEffects and with --filter and found no report. The nearest are #30320 (Windows separators) and #40650 (a list read as false).

The same project in 13 folders. A package in node_modules has "sideEffects": ["./polyfill.js", "./src/*.setup.js"], and a workspace root has packages/a and packages/b. Release builds of 1.4.3 (c6da4a4), 1.4.4 (38afee6) and main (canary a8fcd31), and a debug build of this PR. esbuild 0.21.5 keeps the file in every folder.

folder name 1.4.3 1.4.4 and main this PR
app, app (copy), app(1) right right right
app[1], [tenant], app{a,b} wrong wrong right
+(app), c++(old), app@(v2), @(scope), *(app), ?(app) right wrong right
!(app) right right right

"Wrong" is all of these together: bun build drops src/a.setup.js at exit 0, bun run --filter ./packages/a and --filter '{packages}' exit 1 with no match, and --filter '!./packages/a' runs both workspaces. !(app) is right on main because the name itself matches "not app". The groups entered src/glob with #44805 and #45043, which are in 1.4.4 and not in 1.4.3.

Measured. Release builds of the earlier merge base 5866b5f and of this PR at that base, same toolchain. The numbers were not measured again after the merge of main at 5e18294. In these functions that merge only replaces the joins at parse time with length-checked joins, on both sides. An instruction count is a gdb stepi count on one thread, from the call of the named function to its return, with everything that it calls. A call count is a gdb breakpoint hit count.

  • sideEffects lookup, 200 bare imports of a 16-glob package: primary_side_effects calls 401 -> 401. malloc, memcpy and free calls in it: 400 each -> 0. The PR has no allocator call site in the lookup.
  • Instructions per lookup (primary_side_effects), for the last two lookups of 200 Bun.resolveSync calls. The two are equal. The package directory has D bytes and the list has G globs:
list main PR
no sideEffects field 29 29
false 35 32
16 exact entries 228 228
D=60, G=1 1,938 237
D=180, G=1 5,174 266
D=180, G=16 71,444 2,246
  • Per lookup main runs malloc (17 instructions), memmove (20 at D=180) and operator delete[] (23). The PR runs one memcmp (44 at D=180, 15 at D=60). The table includes them.
  • A bare import of a package with a glob list runs the lookup twice, as on main.
  • finalize_result for one import from a package without a sideEffects field: 1,219 -> 1,216 instructions.
  • A vue 3 + element-plus 2.14.7 build (1,573 modules, 6,472 resolves, 10,784 lookups): 34.1M -> 6.8M instructions in the lookup and the glob matcher (qemu-user, -one-insn-per-tb, two runs differ by less than 200). main.js and main.css are byte-identical.
  • size_of::<SideEffects>(): 80 -> 80 bytes. size_of::<PackageJSON>(): 656 -> 656 bytes. A stored glob is 48 bytes (was 40). Its pattern text no longer holds the directory, so it is shorter by the length of the directory.
  • Release text: 106,331,459 -> 106,325,827 bytes (size). primary_side_effects: 5,446 -> 1,378 bytes plus 1,237 for the glob loop (nm -S).
  • --filter, instructions in workspace_selection::select:
selector 1 workspace 200 workspaces
a name (p0, p7) 989 -> 1,016 20,520 -> 20,347
./packages/p0, ./packages/p7 2,640 -> 3,064 154,861 -> 87,072
{packages} 3,101 -> 3,269 284,135 -> 86,777
  • !./packages/p7 over 200 workspaces: 164,428 -> 95,838. Only the first path selector of a command joins the working directory.

Plain directories. With release builds of both sides at the earlier merge base: 402 builds (67 glob entries in 6 plain directory names, 7 files each) keep the same files, and 2,670 bun run --filter runs (89 path selectors, 5 working directories, 6 plain directory names) select the same workspaces. Under 11 bracket, brace and extglob directory names the PR build gives the answer of the plain directory in all 737 builds and 4,895 runs. One more check is not in the tree: a script compared the joined glob with the split form through Bun.Glob for 1,000,000 random (plain directory, glob, path) rounds. The two forms gave the same answer in every round, and 270,404 rounds were matches.

Tests. 67 new cases. 16 are in dce.test.ts: 13 fail on main, and 3 are pins that pass there (the spellings in a plain directory, the main outside the package in a plain directory, and the absolute specifier). 48 are in filter-workspace.test.ts and 3 in bun-add-filter.test.ts, and all of those fail on main. The directories above the project are named [client], {old,new}, {1..3}, @(client), c++(old), [ws], {a,b} and @(scope). Fail-before ran on the canary release build of main at a8fcd31. Debug build after the merge of main: dce.test.ts 103 pass, filter-workspace.test.ts 179 pass, bun-add-filter.test.ts 147 pass, bundler_barrel.test.ts 70 pass, multi-run.test.ts 132 pass, and the three oversized sideEffects cases of resolve.test.ts pass. bun-add-filter.test.ts has the cases for [ws] and {a,b} only. With a c++(old) case too, tests next to it reached the 5 s limit in 3 of 4 runs of the file on the debug build, and in 0 of 4 runs without it. bun-pm-licenses.test.ts and bun-prune.test.ts: 1 to 3 tests per run reach the 5 s limit on the debug build, different ones each run, and each passes when it runs alone. bun run rust:miri -p bun_glob: 2 tests pass, with one pair of rows for each of the five groups. A plain cargo test -p bun_glob does not link on main, so the unit tests run under Miri only. Nothing was run on Windows by hand: the separator handling in strip_base and the absolute-specifier case come from reading the resolver. The Windows lanes in CI run the three test files. * and ? cannot be in a Windows file name, so *(x) and ?(x) are in the unit test only. One claim in the self-review (bun check <dir>) did not reproduce.

Not changed.


no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/run/filter-workspace.test.ts, test/cli/install/bun-add-filter.test.ts

…in sideEffects globs and --filter paths

A "sideEffects" glob is joined onto the package directory, and a
`--filter` path selector onto the directory the command runs in. The
joined string was matched as one glob, so a `[` or `{` in a directory
name was glob syntax and the directory did not match itself.
`bun build` then dropped a file that the list names, and
`--filter ./packages/a` found no workspace.

`bun_glob::literal_base_len` finds where the joined string stops being
the directory. Both callers compare that part as bytes
(`bun_glob::strip_base`, which takes `/` and `\` as one separator on
Windows) and give only the rest to the matcher.

A path selector that spells a workspace directory byte for byte
selects that directory, beside what the selector matches as a glob.

The "sideEffects" lookup no longer copies the path for each call. A
name selector does not make `--filter` resolve the working directory.

`bun_glob` joins the crates that `bun run rust:miri` runs, so CI runs
its two unit tests.
…mes have glob characters

- `bun build` keeps a file that a "sideEffects" glob lists when the
  package directory, or a directory above it, is named `[client]`,
  `{old,new}`, `a[b` or `a{b`, for the package itself, a dependency,
  a mixed list, entries that go above the package with `..`, and a
  "main" outside the package.
- `--filter` path selectors find their workspaces under `[ws]` and
  `{a,b}`, and select a workspace directory named `[x]` from inside it
  and from other directories, with and without a directory named `x`
  beside it, for `bun run`, `bun add` and `bun remove`.
- Three cases pin answers that do not change: eleven "sideEffects"
  spellings and a "main" outside the package in a plain directory, and
  an absolute import specifier.
@robobun

robobun commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

  • Reproduced with a script that builds one small project in 13 folders: app, app (copy), app(1), app[1], [tenant], app{a,b}, +(app), c++(old), app@(v2), @(scope), !(app), ?(app) and *(app). A package in node_modules has "sideEffects": ["./polyfill.js", "./src/*.setup.js"], and the workspace root has packages/a and packages/b.
  • On 1.4.3 (c6da4a4d3) the folders with a bracket or brace group are wrong: bun build drops src/a.setup.js at exit 0, and bun run --filter ./packages/a present prints error: No workspace packages matched the filter "./packages/a".
  • On 1.4.4 (38afee6c8) and on main (canary a8fcd319b) the folders with +(, @(, *( and ?( are wrong in the same way. 1.4.3 is right in those.
  • With this PR, merged with main at 5e182948be, every folder gives the rows of app.
  • CI on 851c4dcaa4 (build 131972): all 179 jobs that ran pass, and no test is red. The tests that failed once passed on retry, and none of them is in this PR. Two darwin any x64 test jobs still wait for an agent. In build 126660, on 2026-10-16, the same two jobs expired with no agent. The diff is green on every lane that ran, Windows and macOS aarch64 included.
  • PR: Do not read the directory of a sideEffects glob or a --filter path as glob syntax #44939

@robobun

robobun commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:03 AM PT - Oct 24th, 2026

✅ @robobun, your commit 851c4dcaa4463637e3aa9437df309c75b42ca2e0 passed in Build #131972! 🎉


🧪   To try this PR locally:

bunx bun-pr 44939

That installs a local version of the PR into your bun-44939 executable, so you can run:

bun-44939 --bun

@coderabbitai

coderabbitai Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 6 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 3f69319 and 851c4dc.

Files selected for processing: 9
  • scripts/rust-miri.ts
  • src/glob/lib.rs
  • src/glob/matcher.rs
  • src/install/PackageManager/workspace_selection.rs
  • src/resolver/package_json.rs
  • src/resolver/resolver.rs
  • test/bundler/esbuild/dce.test.ts
  • test/cli/install/bun-add-filter.test.ts
  • test/cli/run/filter-workspace.test.ts

No actionable comments were generated in the recent review.

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Essentials
  • Run ID: 94084fbd-c09f-4114-8d53-ee962dca30e3
📥 Commits

Reviewing files that changed from the base of the PR and between 71d0d43 and 2d61800.

📒 Files selected for processing (9)
  • scripts/rust-miri.ts
  • src/glob/lib.rs
  • src/glob/matcher.rs
  • src/install/PackageManager/workspace_selection.rs
  • src/resolver/package_json.rs
  • src/resolver/resolver.rs
  • test/bundler/esbuild/dce.test.ts
  • test/cli/install/bun-add-filter.test.ts
  • test/cli/run/filter-workspace.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 4 remain after this review.

Walkthrough

Glob matching now treats directory prefixes as literal paths and matches patterns against the remaining path. Workspace selectors and package side-effects checks use this behavior, with added tests for paths containing glob syntax.

Changes

Literal-base glob matching

Layer / File(s) Summary
Literal-base glob helpers
src/glob/lib.rs, src/glob/matcher.rs, scripts/rust-miri.ts
Adds helpers to find and strip a literal base path before matching a glob. Tests cover component boundaries, separator handling, and paths outside the base. The matcher documentation describes the full-pattern behavior, and the Miri crate list includes bun_glob.
Workspace path selector matching
src/install/PackageManager/workspace_selection.rs, test/cli/install/bun-add-filter.test.ts, test/cli/run/filter-workspace.test.ts
Workspace selectors retain a literal base and glob tail. Path and subtree matching use that base. Tests cover glob metacharacters in working and workspace directory names.
Package side-effects glob matching
src/resolver/package_json.rs, src/resolver/resolver.rs, test/bundler/esbuild/dce.test.ts
Side-effects glob entries store a pattern tail and package-path base length. Resolver checks use PackageJSON::has_side_effects. Tests cover path spellings, directory names, and extension rewriting.

Suggested reviewers: jarred-sumner, dylan-conway

Priority: Normal

Merge Risk: Minimal · up to 2d618

The change is mergeable after normal checks; no actionable matching failure was established.

Pre-merge checks: Passed 4
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Title check Passed The title clearly describes the main change: directory names are no longer interpreted as glob syntax for sideEffects globs or --filter paths.
Description check Passed The description explains the problem, fix, scope, known limitations, performance impact, and extensive verification results. It does not use the template headings exactly, but it provides the required…
Full details: Description check

Explanation

The description explains the problem, fix, scope, known limitations, performance impact, and extensive verification results. It does not use the template headings exactly, but it provides the required information through the Problem, Fix, and Tests sections.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline finding, I also checked the Base::Subtree ancestor walk in workspace_selection.rs: the new base.len() + i > 0 stop reduces to main's i > 0 when the selector climbed to / (empty base) and otherwise stops at the base directory itself, which main also tested as a glob candidate, so the selected set is unchanged for plain directories. The base_len as u32 cast in SideEffectsGlob::init is bounded by the package.json path length, and the literal_base_len unit cases cover root, empty, and ..-climbing inputs.

Extended reasoning...

The change replaces joined-glob matching with a literal-base-plus-glob-tail split in two sites (sideEffects globs in the resolver and path selectors in --filter), adds three helpers to bun_glob with unit tests, and extends three test files. It touches no auth, crypto, or network surface. One confirmed finding (candidate path no longer slash-normalized on POSIX) is posted inline; the ancestor-walk and index-arithmetic concerns I checked were equivalent to main.

Comment thread src/resolver/package_json.rs Resolved
The glob matcher on main is new: patterns are prepared once
(bun_glob::Prepared) and have more syntax. The tail of a sideEffects
glob and of a path selector is now a prepared pattern, and the match
under a base is Prepared::match_under.
The matcher on main now reads an unclosed `[` or `{` as itself, so
`a[b` and `a{b` match on main too. `{1..3}` and `@(client)` are new
syntax there and do not match themselves.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed the updated revision and didn't find any bugs. Because it changes how every sideEffects glob and --filter path selector is matched, a human look would still be worthwhile.

What was reviewed:

  • literal_base_len cuts only at a component boundary of both strings, so the tail handed to match_under is always empty or starts with a separator (the debug_assert cannot be reached by user input), and sibling directories (/t/app vs /t/app2) are rejected because that leading / must match a separator.
  • package_json.rs: self.source.path is the same path the entry was cut against at parse time (PathName::dir is a prefix of text, including the drive-letter case), and on Windows the \ candidate path matches the /-normalized tail through the matcher's WINDOWS mode.
  • workspace_selection.rs Subtree climb stops at the base, or at the root when the base is empty, matching the old i > 0 guard; other Prepared::new/glob::r#match callers (pack, test, archive, update_scope, negated workspaces) match names or relative globs, not a directory-joined pattern.
Extended reasoning...

The change adds three primitives to the bun_glob crate and reworks how package.json "sideEffects" globs and --filter path selectors are stored and matched, replacing whole-string glob matching of a joined path with a literal directory-prefix compare plus a glob tail; it also moves has_side_effects onto PackageJSON and updates the resolver caller. It touches no auth, crypto, or injection surface, but it alters tree-shaking decisions for every package with glob sideEffects and the selection semantics of --filter, including a new rule that a selector spelling a workspace directory selects it. The bug hunt ran dry and the new commits since the prior review (merge of main plus a test adjustment) did not change the core logic, but the breadth of affected behavior and the untested Windows separator path make a human look worthwhile rather than approval.

…-literal-base

# Conflicts:
#	src/install/PackageManager/workspace_selection.rs
#	src/resolver/package_json.rs
…nd --filter paths

Since 1.4.4 the matcher reads ?( ), *( ), +( ), @( ) and !( ) as groups in every
pattern, so a directory with such a name hits the same fault as [client] and
{old,new}. The unit test has one pair of rows for each of the five groups.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

Every test in bun-add-filter.test.ts runs concurrently, and the c++(old) row
adds three more installs. On a debug build, tests next to it then go past the
5 s limit in 3 of 4 runs of the file, and in 0 of 4 runs without the row.

filter-workspace.test.ts and dce.test.ts keep the c++(old) and @(scope) cases,
and the selection code is the same for bun run and bun add.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment thread test/cli/install/bun-add-filter.test.ts Resolved
The cases next to them compare the whole stderr, and the run helper removes
the progress lines for that purpose. A warning or any other line now fails
the case, and a failure shows the text that the command printed.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed the latest push and found no bugs; the exact-stderr assertions in bun-add-filter.test.ts now match their siblings. Because this changes what bun build tree-shakes and what --filter selects (including the deliberate choice that ./packages/[x] selects both [x] and x), a human look at that design call is still worthwhile.

What was reviewed:

  • literal_base_len/strip_base consistency between parse time (json_source.path.name().dir) and match time (self.source.path.text[..base_len]): same path text is copied into package_json.source, so the prefix is identical.
  • Windows separators: the tail is posix-normalized and match_prepared::<{cfg!(windows)}> treats \ as a separator in the candidate, so dropping normalize_path_for_glob on the candidate is equivalent there.
  • Base::Subtree walk-up bound (base.len() + i > 0) reaches the base itself but not /, matching main's i > 0 for a plain cwd; root-cwd {.} still misses children, as on main.
Extended reasoning...

The diff adds literal_base_len, strip_base and Prepared::match_under to src/glob/lib.rs with a cfg(test) module, and uses them in src/resolver/package_json.rs (sideEffects globs stored as a literal base length plus a glob tail, has_side_effects moved onto PackageJSON) and src/install/PackageManager/workspace_selection.rs (path and subtree selectors keyed on the cwd prefix), plus tests in dce.test.ts, bun-add-filter.test.ts and filter-workspace.test.ts. It touches no auth, crypto or injection surface; the untrusted inputs are package.json strings and CLI args, and all slicing is bounded by literal_base_len which never exceeds either input. Deferring rather than approving because the change alters bundler DCE and workspace selection semantics and includes a user-visible design decision (a bracketed selector also matches the unbracketed sibling) that a maintainer should weigh; the only prior inline nit was addressed by the newest commit.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants