Skip to content

pack: always include "main" and "browser" entry points in the tarball - #36266

Closed
robobun wants to merge 6 commits into
mainfrom
farm/545bd4ee/pack-force-include-main-browser
Closed

robobun wants to merge 6 commits into
mainfrom
farm/545bd4ee/pack-force-include-main-browser

Conversation

@robobun

@robobun robobun commented Jul 28, 2026 •

Copy link
Copy Markdown
Collaborator

What

bun pm pack now force-includes the files referenced by package.json's "main" and string-form "browser" fields, matching npm pack. Previously only "bin" targets were protected, so a "files" allowlist that omitted the entry point (or an .npmignore that matched it) produced a tarball whose "main" pointed at a file that was not shipped.

Repro

mkdir -p /tmp/mainless/dist /tmp/mainless/lib && cd /tmp/mainless
printf '{"name":"mainless","version":"1.0.0","main":"lib/index.js","browser":"lib/browser.js","files":["dist"]}' > package.json
echo 'exports.b=1' > dist/bundle.js
echo 'module.exports="entry"' > lib/index.js
echo 'module.exports="browser"' > lib/browser.js
bun pm pack && tar -tf mainless-1.0.0.tgz

Before: tarball contains only package/package.json and package/dist/bundle.js. lib/index.js and lib/browser.js are missing, so require("mainless") fails after install.

After: tarball also contains package/lib/index.js and package/lib/browser.js, same as npm pack.

The .npmignore variant behaves the same way: "bin" was kept but "main" was dropped.

Cause

is_unconditionally_included_file in src/runtime/cli/pack_command.rs covers package.json, LICENSE*, LICENCE*, and README*; bin targets are protected separately via get_package_bins / is_package_bin. There was no rule for "main" or "browser". npm-packlist's processPackage appends strict !/${main} / !/${browser} rules alongside the bin rules.

Fix

Add get_package_entry_points, which extracts "main" and the string form of "browser" (the object-map form is a remap table, not a file path), normalizes each path, and filters out values that duplicate a "bin" target or each other. These are queued as optional: true (a missing file is not an error, matching bin handling) and deduped against the three tree walks so an entry point that is also matched by "files" or not ignored appears exactly once. Entry points do not pick up the executable bit that bin targets get.

Verification

New entry points describe block in test/cli/install/bun-pack.test.ts:

  • "main" + "browser" are included when "files" omits them
  • "main" is included when .npmignore matches it, and does not gain +x
  • "main" already inside the "files" tree is not duplicated
  • a "main" that does not exist on disk is not an error
  • object-form "browser" is not force-included (npm compat)

Full bun-pack.test.ts suite passes (81 tests).

Fixes #15602


[review] gate passed · iteration 1 · 2 files touched

fails on main (without fix)
ASAN without fix: 4 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/cli/install/bun-pack.test.ts
bun test v1.4.0 (5f018c639)

test/cli/install/bun-pack.test.ts:
(pass) basic [180.72ms]
(pass) in subdirectory [333.43ms]
(pass) package.json names and versions > rejects name and version containing parent directory components [466.40ms]
(pass) package.json names and versions > missing name [147.78ms]
(pass) package.json names and versions > missing version [140.92ms]
(pass) package.json names and versions > missing name and version [138.11ms]
(pass) package.json names and versions > empty name [135.09ms]
(pass) package.json names and versions > empty version [140.50ms]
(pass) package.json names and versions > empty name and version [146.17ms]
(pass) package.json names and versions > missing [120.96ms]
(pass) package.json names and versions > scoped name: @scoped/pkg [169.55ms]
(pass) package.json names and versions > scoped name: @ [156.46ms]
(pass) package.json names and versions > scoped name: @/ [157.85ms]
(pass) package.json names and versions > scoped name: // [159.66ms]
(pass) package.json names
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (c2ee42d36)

test/cli/install/bun-pack.test.ts:
(pass) basic [6.25ms]
(pass) in subdirectory [10.87ms]
(pass) package.json names and versions > rejects name and version containing parent directory components [11.02ms]
(pass) package.json names and versions > missing name [3.66ms]
(pass) package.json names and versions > missing version [2.27ms]
(pass) package.json names and versions > missing name and version [2.71ms]
(pass) package.json names and versions > empty name [2.37ms]
(pass) package.json names and versions > empty version [2.66ms]
(pass) package.json names and versions > empty name and version [2.22ms]
(pass) package.json names and versions > missing [1.96ms]
(pass) package.json names and versions > scoped name: @scoped/pkg [5.10ms]
(pass) package.json names and versions > scoped name: @ [5.04ms]
(pass) package.json names and versions > scoped name: @/ [4.65ms]
(pass) package.json names and versions > scoped name: // [5.05ms]
(pass) package.json names and versions > scoped name: @// [4.29ms]
(pass) package.json names and versions > scoped name: @/s [5.11ms]
(pass) package.json names and versions > scoped name: @s [4.75ms]
(pass) fl
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/cli/install/bun-pack.test.ts
bun test v1.4.0 (5f018c639)

test/cli/install/bun-pack.test.ts:
(pass) basic [183.60ms]
(pass) in subdirectory [331.23ms]
(pass) package.json names and versions > rejects name and version containing parent directory components [453.60ms]
(pass) package.json names and versions > missing name [136.13ms]
(pass) package.json names and versions > missing version [147.13ms]
(pass) package.json names and versions > missing name and version [143.35ms]
(pass) package.json names and versions > empty name [141.38ms]
(pass) package.json names and versions > empty version [135.33ms]
(pass) package.json names and versions > empty name and version [145.32ms]
(pass) package.json names and versions > missing [117.11ms]
(pass) package.json names and versions > scoped name: @scoped/pkg [165.90ms]
(pass) package.json names and versions > scoped name: @ [153.06ms]
(pass) package.json names and versions > scoped name: @/ [152.26ms]
(pass) package.json names and versions > scoped name: // [155.48ms]
(pass) package.json names
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 706ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/6] gen generated_host_exports.rs
generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 240 extern-C blocks audited
[1/6] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m   Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m   Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
�[1m�[92m   Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
�[1m�[92m   Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
�[1m�[92m   Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
�[1m�[92m   Compiling�[0m bun_output v
... (truncated)
diff hotspot
src/runtime/cli/pack_command.rs   |  94 +++++++++++++++++++-
 test/cli/install/bun-pack.test.ts | 179 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 271 insertions(+), 2 deletions(-)

gate history · 4 passed · 0 rejected · iteration 1

evidence per changed file
file                               reads  edits  tests
src/runtime/cli/pack_command.rs        0      0      0
test/cli/install/bun-pack.test.ts      2      3      0

npm-packlist emits strict un-ignorable rules for package.json's "main"
and string-form "browser" fields, so those files land in the tarball
even when a "files" allowlist omits them or .npmignore matches them.
bun pm pack already did this for "bin" targets but not for "main" or
"browser", so a package whose files list forgot its own entry point
would ship a tarball that resolves main to a missing file.

Mirror the existing bin handling: extract the two entry point paths,
queue them as optional items (a missing file is not an error), and
dedupe against the tree walk so a main that is also matched by files
or not ignored appears exactly once.
@robobun

robobun commented Jul 28, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 9:20 PM PT - Jul 28th, 2026

❌ @robobun, your commit 5f018c6 has 1 failures in Build #84482 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 36266

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

bun-36266 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. bun pm pack does not handle .npmignore correctly #15602 - .npmignore patterns can exclude entry point files; this PR's force-include of "main" and "browser" fields ensures those entry points are never excluded

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #15602

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

bun pm pack now separately includes valid main and string-form browser entry points, prevents duplicate traversal, excludes package.json bin targets, and skips missing or non-file optional entries during archive generation and dry runs.

Changes

Package entry-point packing

Layer / File(s) Summary
Entry-point resolution
src/runtime/cli/pack_command.rs
main and string-form browser targets are normalized, filtered, deduplicated, and separated from bin targets.
Queue and traversal integration
src/runtime/cli/pack_command.rs
Entry points are queued as optional files, while project-tree traversal skips them to prevent duplicate archive entries.
Archive validation and coverage
src/runtime/cli/pack_command.rs, test/cli/install/bun-pack.test.ts
Optional entries are packed or listed only when they are regular files, with tests covering inclusion, ignore handling, missing targets, directories, duplicate package.json, and browser maps.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The change addresses #15602 by preserving required entry points under .npmignore rules and adds coverage for the reported behavior.
Out of Scope Changes check ✅ Passed The extra path normalization, deduplication, and non-regular-file handling support the stated pack-entry-point fix, with no unrelated changes.
Title check ✅ Passed The title is concise and matches the main change to pack main/browser entry points.
Description check ✅ Passed The description covers the change, repro, fix, and verification, which satisfies the template’s intent.

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

@robobun

robobun commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

Confirmed this fixes #15602. That report's .npmignore is:

*
!dist

and npm's output there is package.json, README.md, dist/cli.js, src/index.ts. Under gitignore semantics * ignores every entry at every depth and !dist only un-ignores the dist directory entry itself, so dist/other-files stay ignored in npm too. What npm actually shipped was the force-included set: dist/cli.js because it is the bin target and src/index.ts because it is the main target. bun already kept the bin target; with this change it keeps main as well, and the tarball matches npm's for that repro.

Comment thread src/runtime/cli/pack_command.rs
When main/browser/bin names a directory, openat succeeds on POSIX so
the ENOENT guard is bypassed and read() later fails with EISDIR. Check
S_ISREG after fstat and skip optional items that are not regular files.
npm does not force-include a directory main either, so skipping matches
npm here. Also fixes the pre-existing crash for string bin targets that
name a directory.
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
package.json is written out of band by archive_package_json (with
workspace: protocol rewriting applied) before the drain loop. Both tree
walks already skip it at depth 1 for that reason; the force-queued bin
and main/browser paths bypass the tree walks, so a main/browser/bin
value of "./package.json" produced a duplicate tarball entry (the
second being the raw on-disk bytes, overwriting the edited copy on
extraction). Filter package.json out when collecting bins and entry
points.

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/runtime/cli/pack_command.rs`:
- Around line 2747-2756: Update the optional-path handling in the pack command
so non-regular paths are rejected before the blocking or side-effectful openat
operation; use a pre-open metadata/type check, or an explicitly safe nonblocking
open-and-validate sequence that prevents FIFOs from blocking and device nodes
from being opened. Preserve the existing stats decrement, progress completion,
and continue behavior for skipped optional paths.
- Around line 2343-2347: Update the entry-point handling around PackQueueItem
creation so optional entries suppress only ENOENT; propagate permission, I/O,
malformed-path, and all other filesystem errors. Preserve successful packing and
the missing-file behavior for entries marked optional, using the existing
PackQueueItem/P​​ackQueue flow and error classification mechanisms.
- Around line 2343-2347: Update the entry-point queuing flow around
PackQueueItem and the later package-file open operation to resolve paths beneath
root_dir without following symlinks, including symlinked parent components. Use
the platform’s no-follow or component-safe openat resolution flags/API, while
preserving regular-file validation and existing PackQueueItem behavior.

In `@test/cli/install/bun-pack.test.ts`:
- Around line 1632-1751: Extend the entry-point edge-case tests around the
existing main-focused cases to also cover string-form browser entries.
Parameterize the missing-file, directory, deduplication, and ignore-rule
scenarios over main and browser, or add equivalent browser tests, while
preserving the current assertions for each generated tarball.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a69a179d-4065-4aab-b449-0c112c92bec4

📥 Commits

Reviewing files that changed from the base of the PR and between e532ad9 and c2ee42d.

📒 Files selected for processing (2)
  • src/runtime/cli/pack_command.rs
  • test/cli/install/bun-pack.test.ts

Comment thread src/runtime/cli/pack_command.rs
Comment thread src/runtime/cli/pack_command.rs
Comment thread test/cli/install/bun-pack.test.ts

@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 — both issues from earlier passes (directory main crash, package.json duplicate) are fixed with tests, and this run's bug hunt found nothing new.

What was reviewed:

  • get_package_entry_points mirrors get_package_bins exactly (same normalize_buf::<Posix>, bin_path_escapes_root, package.json filter).
  • is_package_entry_point dedup wired into all three tree walks at the same site as the bin-file dedup; entry_subpath uses / on all platforms so the comparison holds on Windows.
  • The !ISREG skip is applied in both the real drain and the dry-run drain, and mirrors the pre-existing optional open-error handling for bins.
  • Entry points inside a directories.bin tree still pick up +x via is_package_bin in add_archive_entry.
Extended reasoning...

Overview

Adds npm-packlist-compatible force-inclusion of "main" and string-form "browser" to bun pm pack, alongside the existing "bin" handling. Touches src/runtime/cli/pack_command.rs (~94 lines: new get_package_entry_points / is_package_entry_point, three tree-walk dedup call sites, force-queue loop, !ISREG skip in both drain loops, and a package.json filter added to get_package_bins) and test/cli/install/bun-pack.test.ts (7 new tests in an entry points describe block).

Security risks

None material. Entry-point paths come from the package's own package.json, are normalized with the same resolve_path::normalize_buf::<Posix> used for bins, and are rejected by bin_path_escapes_root if absolute or ..-escaping. openat is relative to the package root fd. Symlink-following and swallow-all-errors-when-optional are pre-existing bin semantics reused here, not new surface.

Level of scrutiny

Medium. This is a user-facing CLI behavior change affecting published tarball contents, but it is a strict superset of prior output (adds files that npm already includes), closely mirrors the established bin path, and is verified against npm-packlist semantics. It went through two prior review rounds where I flagged a directory-main crash regression and a package.json duplication — both were fixed (c897db0, c2ee42d) with regression tests. CodeRabbit's remaining points (ENOENT-only suppression, symlink traversal, FIFO-before-open) were correctly declined as pre-existing bin-drain semantics out of scope for this npm-compat change.

Other factors

  • Full bun-pack.test.ts suite (81 tests) passes on debug+ASAN and release; the new tests fail on main without the fix per the PR's evidence block.
  • Test coverage is thorough: files allowlist, .npmignore, dedup with files, missing file, package.json self-reference, directory-valued main, object-form browser, and the no-+x invariant.
  • All inline review threads are resolved. No human reviewer has weighed in with unaddressed concerns.
  • I confirmed entry_subpath builds paths with / on every platform, so the strings::eql_long dedup against Posix-normalized entry points is sound on Windows.

@robobun

robobun commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

CI status: the diff is green. bun-pack.test.ts (83 tests, including the 7 new entry points cases) passes on every lane across both builds #84466 and #84482.

Remaining red on #84482 is unrelated to bun pm pack / pack_command.rs:

  • [new] test/cli/run/no-orphans.test.ts (orphan-process tracking in bun run; reported separately)
  • 17 [flaky] tests (spawn-streaming-stdout, watch-many-dirs, numeric-header, bun-install-registry, complex-workspace, proxy-stress-protocol, socket-retention, 08965, 22199, setInterval, 20875, request-smuggling, 09279, svelte client-side, 20144, fetch-tls-abortsignal-timeout, streams-leak), all of which passed on retry or passed when run alone

The [new] failure on #84466 was worker-transfer-terminate-stress.test.ts (JSC ExceptionScope::assertNoException SIGABRT on x64-asan), also unrelated and reported separately.

Ready for review.

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

robobun commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-28, it conflicts with main, and its last CI run failed. This is not a judgment on the fix itself. The linked issue (#15602) stays open. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main.

@robobun robobun closed this Sep 13, 2026
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