Skip to content

pack: honor .npmignore negation patterns that reach inside an excluded directory - #36699

Open
robobun wants to merge 4 commits into
mainfrom
claude/farm/35752c84/pack-npmignore-negation
Open

robobun wants to merge 4 commits into
mainfrom
claude/farm/35752c84/pack-npmignore-negation

Conversation

@robobun

@robobun robobun commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes #15602.

Problem

With an .npmignore like:

*
!dist/**

bun pm pack prunes dist/ at the directory level (because * matches it) before !dist/** ever gets a chance to re-include the files inside. npm handles this correctly via ignore-walk, which does a minimatch partial test against negated rules when deciding whether to descend into a directory.

$ mkdir -p dist src && echo a > dist/cli.js && echo b > src/index.ts
$ printf '{"name":"pkg","version":"0.0.7"}' > package.json
$ printf '*\n!dist/**\n' > .npmignore

$ bun pm pack --dry-run   # before: only package.json
$ npm pack --dry-run      # dist/cli.js, package.json

The same applies to !dist/*, !dist/**/*, !dist/lib/foo.js, !*/keep.js, and !**/keep.js.

Fix

is_excluded now checks, for directory entries only: if a negated pattern doesn't match the directory itself but could match a path beneath it (segment-wise prefix match, equivalent to minimatch's partial), keep the directory in the walk. Files inside are still filtered individually, so only paths a negation actually matches end up in the tarball. Pattern order is preserved, so a later positive rule (e.g. * / !dist/** / dist/sub) still re-excludes correctly.

Verification

New parameterized tests in test/cli/install/bun-pack.test.ts cover ten .npmignore variants; each expected file list was taken from npm pack --dry-run (npm 11.16.0) on the same fixture. 7 of the 10 fail on current main and all 10 pass with this change. The full bun-pack.test.ts suite (86 tests) passes.

…d directory

With an .npmignore like:

    *
    !dist/**

bun pm pack pruned dist/ at the directory level (because * matches it)
before !dist/** ever had a chance to re-include the files inside. npm
(via ignore-walk) handles this by doing a partial match against negated
rules when deciding whether to descend into a directory.

is_excluded now does the same: when evaluating a directory entry, a
negated pattern that doesn't match the directory itself but could match
a path beneath it (segment-wise prefix match) keeps the directory in the
walk. Files inside are still filtered individually, so only paths that a
negation actually matches end up in the tarball.

Fixes #15602
@coderabbitai

coderabbitai Bot commented Aug 1, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 1 minute

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f5d30c90-2ac1-430b-97f0-7fbdf4c7ddd2

📥 Commits

Reviewing files that changed from the base of the PR and between 4e72961 and 9ce36db.

📒 Files selected for processing (1)
  • src/runtime/cli/pack_command.rs

Walkthrough

Changes

Bun pack now traverses excluded directories when negated .npmignore patterns may re-include descendants. Regression tests compare packed paths for multiple wildcard, recursive, nested, and file-specific patterns.

npmignore negation handling

Layer / File(s) Summary
Preserve directories for descendant negations
src/runtime/cli/pack_command.rs
is_excluded evaluates whether negated patterns can match paths beneath excluded directories, including relative segments and leading **/ patterns.
Validate npmignore packing behavior
test/cli/install/bun-pack.test.ts
Parameterized tests compare packed paths for wildcard, recursive, file-specific, nested, and subsequent-exclusion patterns.

Possibly related PRs

  • oven-sh/bun#36266: Both modify bun pm pack file-tree inclusion and .npmignore handling for npm-compatible behavior.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the fix for .npmignore negation patterns inside excluded directories.
Description check ✅ Passed The description explains the problem, implementation, and verification with relevant examples and test results.
Linked Issues check ✅ Passed The changes address issue #15602 by preserving traversal for negated .npmignore patterns and including matching package contents.
Out of Scope Changes check ✅ Passed The code changes and regression tests are directly related to the linked issue and stated pull request objectives.

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

@github-actions github-actions Bot added the claude label Aug 1, 2026
@robobun

robobun commented Aug 1, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 3:59 AM PT - Aug 1st, 2026

⏳ @robobun, your commit 9ce36db is still building in Build #87103, but has 2 failures so far (All Failures):

@robobun

robobun commented Aug 1, 2026 •

Copy link
Copy Markdown
Collaborator Author

Reproduced with .npmignore = * + !dist/** (and !dist/*, !dist/**/*, !dist/lib/foo.js, !*/keep.js, !**/keep.js): bun pm pack pruned dist/ before the negation could re-include its contents. Fixed by partial-matching negated patterns against directory paths in is_excluded.

All 86 bun-pack.test.ts tests pass locally and in CI. The remaining CI failures on build 87103 are unrelated (bun-upgrade.test.ts on windows-aarch64 where canary builds don't exist, plus pre-existing/flaky tests on other lanes). Ready for review.

Comment thread src/runtime/cli/pack_command.rs Outdated
Comment thread src/runtime/cli/pack_command.rs Outdated
Comment thread src/runtime/cli/pack_command.rs Outdated
Comment thread src/runtime/cli/pack_command.rs Outdated
Comment thread src/runtime/cli/pack_command.rs Outdated
Comment thread src/runtime/cli/pack_command.rs Outdated

@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.

LGTM — focused fix that keeps the pack walk from pruning directories a later !pattern reaches into.

What was reviewed:

  • Traced partial_path_match segment-by-segment against each of the ten test fixtures (!dist/**, !dist/*, !dist/lib/foo.js, !*/keep.js, !**/keep.js, and the re-exclusion case * / !dist/** / dist/sub); all produce the npm-verified expected sets.
  • Confirmed the new branch is gated on is_dir && ignored and only consults rel when the pattern has REL_PATH, so ignore.has_rel_path guarantees rel was trimmed to the ignore file's depth.
  • Checked that node_modules is skipped before pattern matching (lines ~507/737), so !**/… returning true from LEADING_DOUBLESTAR_SLASH cannot cause a walk into it.
  • The files-array include path (~536-580) uses opt-in semantics and is not affected.
Extended reasoning...

Overview

The PR fixes #15602: bun pm pack was pruning directories at the point a positive .npmignore rule (e.g. *) matched them, so a later negated rule like !dist/** never got to re-include their contents. The change adds a second un-ignore branch in is_excluded for directory entries only: if the negated pattern doesn't match the directory itself but could match something beneath it (a segment-wise prefix test mirroring minimatch's partial), the directory stays in the walk. Two small helpers (negated_pattern_matches_under_dir, partial_path_match) implement this — ~45 lines of pure slice logic, no allocation, no unsafe. Ten new parameterized tests in bun-pack.test.ts pin the packed file list for each .npmignore variant to what npm pack produces on the same fixture.

Security risks

None. This only widens directory traversal during bun pm pack in the user's own project; individual files still have to survive the per-file pattern pass to land in the tarball. node_modules and the non-overridable defaults (.git, .npmrc, lockfiles) are filtered before this code runs, so the new branch cannot re-include them. No untrusted input, no path resolution, no FFI.

Level of scrutiny

Moderate. It's a behavioral change to file-selection logic in a CLI subcommand, so the main risk is over- or under-including files relative to npm. I hand-traced the loop for every test case (including the order-sensitive * / !dist/** / dist/sub re-exclusion and the depth-limited !*/keep.js) and each matches the stated npm output. The ignored guard preserves last-rule-wins ordering, and the REL_PATH gate ensures rel is always the correctly-trimmed path when partial_path_match reads it (any REL_PATH pattern forces ignore.has_rel_path = true at parse time).

Other factors

The comment-cop bot flags on earlier commits are all resolved (comments were trimmed in 8afb04d and 9ce36db). The bug-hunting pass found nothing. The full 86-test bun-pack.test.ts suite is reported passing, and 7 of the 10 new cases fail on main — so the tests demonstrably exercise the fix. The package.json files-array path is a separate opt-in walker and doesn't share this pruning bug.

Jarred-Sumner pushed a commit that referenced this pull request Aug 30, 2026
… pack output (#40959)

### Problem
- `test/cli/install/bun-pack.test.ts` takes 10.7s on debian 13 x64-asan
in the serial phase (build 108487). Its 80 tests run one at a time, each
with one to five `bun pm pack` spawns.
- The assertions are loose: the harness `pack()` helper only checks that
stderr lacks `error:`, `warning:`, `failed` and `panic:`, tarballs are
checked with `toMatchObject`, and the `--filename="out/foo.tgz"` error
case accepts any outcome.

### Fix
- Each test builds its tree with `tempDir` instead of the shared
`beforeEach` directory. The describes are `describe.concurrent`, the
top-level tests `test.concurrent`.
- A local `runPack()` returns stdout and stderr, raw and normalized with
`normalizeBunSnapshot`. The normalized stdout masks the shasum, the
integrity and the packed size, which depend on the compressor.
- Every test asserts that `err` is `""` (or the exact `$ script` echo),
the exact stdout, the exit code, and the full entry list with `toEqual`.
Error cases assert the exact message and that nothing was written.
- Verified: local debug+ASAN build, 80 tests in 20.6s and 21.8s before,
83 tests in 6.9s, 6.9s and 7.0s after. `--rerun-each=3` passes 249 of
249. CI debian 13 x64-asan: 10.7s before, 3.0s after (build 108529).

### Background
- `describe.concurrent` runs a group's async tests up to
`--max-concurrency` at a time (20, or 5 in ASAN builds). Groups and
top-level `test.concurrent` tests overlap, so a shared module-level
directory is not safe.
- `toMatchInlineSnapshot` works in concurrent tests, but one call site
cannot hold different values across `test.each` rows. The tables compare
a line array instead.

<details><summary>Notes</summary>

- Test count 80 to 83: `--gzip` is split into three rejected-level cases
and one level 0 vs level 9 case, and the `--filename="out/foo.tgz"`
error row is its own test. No test was removed or skipped.
- `readTarball` from `bun:internal-for-testing` parses a tarball into
its entries, shasum and integrity.
- Lines that use `expect.stringMatching` instead of an exact value: the
package.json size and the unpacked size in the tables whose rows change
package.json (scoped names, `workspace:` specs, `bundledDependencies`
spelling), and in the two lifecycle tests whose scripts embed
`bunExe()`, so the size depends on the path of the bun binary. On the
darwin CI agent that path pushes package.json past 512 bytes and the
size prints as `0.58KB`, so those two matchers accept any size format
(build 108529 caught the `NNNB`-only version).
- The exact output records some current behavior as-is: the name `//`
writes `-1.1.1.tgz` but prints `//-1.1.1.tgz`; the name `@//` fails with
`failed to open tarball file destination: ".../-/-1.1.1.tgz"` (the old
test only asserted a non-zero exit); transitive scoped bundled deps
print without their scope (`bundled dep3` for `@scoped/dep3`);
`--dry-run` prints the on-disk package.json size while a real pack
prints the re-serialized size; empty files print as `0KB`. None of these
is changed here.
- `bun install` still runs once per `workspace:` lockfile case (7 runs).
They are workspace-only and contact no registry. The
`bundledDependencies` tests already built `node_modules` on disk.
- The release binary runs the file in 0.19s locally. Under ASAN each
spawned pack still costs 150 to 400ms, so what remains is CPU bound:
about 85 debug `bun pm pack` runs, 5 at a time.
- Open PRs that add cases to this file (#36266, #38715, #36699, #38813,
#38721, #38739, #38835, #38720, #38749, #38784, #38707, #38716) need a
rebase onto the new shape: a `tempDir` tree plus `runPack(dir)`.
- CI durations before, build 108487 serial phase: 10.7s debian 13
x64-asan, 2.0s windows 11 aarch64, 1.5 to 1.7s alpine, about 1s on the
other release lanes.

</details>

<!-- robobun:evidence:begin -->

---

**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/install/bun-pack.test.ts

<!-- robobun:evidence:end -->

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.

bun pm pack does not handle .npmignore correctly

2 participants