install: fold the open bun install / pm / pack+publish PRs into one branch (171 PRs) - #39403
install: fold the open bun install / pm / pack+publish PRs into one branch (171 PRs)#39403Jarred-Sumner wants to merge 258 commits into
Conversation
…catalogs change (#38849)
…bun update <name> (#38847)
… tarball versions (#38331)
…ut a trailing slash (#38698)
…rn.lock migration (#38795)
…json relative to the package (#38990)
…cal file: package (#38814)
… relative to the package (#38816)
…action folder name (#39011)
…n the path buffers (#38359)
…n the path join buffer (#38368)
… the path buffer (#38390)
…json does not fit the path buffer (#38575)
…nger than the path buffer (#38784)
…arball downloads (#38817)
…le_package_name subsumes; dependency.rs trims (cherry picked from commit 52f11087637146ffaca82549ccb00d890cf6f699)
(cherry picked from commit da762b3a2d9efef269eef974141901794a3d2285)
…kfile::reachable over snapshot-patched resolutions (cherry picked from commit b63643331b0b58a17d666c6e2c7f108f99e1981f)
…eferred field) (cherry picked from commit 5ad0fa213e5010fc7821e8168fbaf3b1a033b487)
…estructuring (cherry picked from commit 8386c70db7dc11902dadcc2a82031a18379bf938)
…ut the redundant pre-scan (cherry picked from commit 1f36ab77d7321dcbaa26e20fc61f56f46126c924)
(cherry picked from commit c4d5f35ad6c0a1593ca638f04c189d1b37c75e23)
(cherry picked from commit 5665d9f6bdbb80e301ba1df47bdb7eb3ed91c44e)
…ocality checks (cherry picked from commit b96464da224ca6d23c6471d8fcc20bbbc32a22be)
(cherry picked from commit 36508548f417e76b906bf6573ea51311a735696a)
(cherry picked from commit 4af66bcd9663549eef04908b2d58e4bedd222c8f)
(cherry picked from commit 51edcf2bf8e693538504d51f78eef52e77cd457a)
…git URL parsing, dedupe package.json parsing - Entry derives Default; the four dependency maps become one array in DEPENDENCY_SECTIONS order, which the parser and the migrator both iterate - parse_git_url/ParsedGitUrl folded into Entry::set_git_source (was duplicated for the version and resolved keys) - root and workspace package.json parsing share append_package_json - package ids are handed out from one (dedupe name, version) map instead of the package_versions/scoped_packages pair - os/cpu list parsing and Meta arch/os construction each written once (cherry picked from commit 903a5f328c680483a59c17dd3712b442b92c8962)
…kspaces object path, drop redundant trusted fill - copy_into_root always merges into a (possibly new) root-level object, so copy_object/shallow_clone_prop go away; the bare patch-key completion that rewrite_bare_patch_keys did happens during that merge - the three branches that wrote `workspaces` (array, existing object, new object) collapse into array-or-object - add_trusted_dependencies no longer fills lockfile.trusted_dependencies: migration::record_trusted_dependencies reads the edited cache entry right after and records the same list - trim multi-line comments (cherry picked from commit 736bc1281c48622f69c7613a1015cbe3fdb89456)
…lder_relative_to_top_level_dir, drop the Scanned enum - WorkspaceManifests::workspace_name_at_path resolves the linked directory with lockfile::package::folder_relative_to_top_level_dir (the same join+relative+posix steps it spelled out) - process_workspace_name returns Option<Entry> and records a skipped nameless member itself, replacing Scanned/workspace_entry - shorten doc comments (cherry picked from commit 2abe25708470a1a78bb233953ccbe2a8258bfb94)
…ditor tightening - CatalogMap::put_missing_from_pnpm_workspace is a method; the recorded default catalog is moved out with mem::take for the `catalogs.default` group instead of splitting get_or_put_group into a free helper - OverrideMap: parse_count walks both fields in one loop (count_field inlined), RuleCount becomes two ParseContext fields, put_scoped keeps push_scoped's original shape with a `keep` bound - migration.rs: finish_migration takes the migrator's Result and builds the Migrating LoadResultErr in one place (migrating_error) - PackageJSONEditor: literal_tag() for the seven infer(trim_literal(..)) sites - shorter doc comments in package_json_write_back (cherry picked from commit 39c13fbb5a8750e6e9184286e451bf93acfeb5cd)
alii
left a comment
There was a problem hiding this comment.
Went through the fold with the head built locally and CI build 99835 pulled apart. Leaving these as comments, not a block, since the shape is Jarred's call.
Two things exist only because of the fold and are worth knowing before anything else: the isolated linker now panics on any link: dependency (#38045 x #38369, thread below), and lifecycle script counting on relink hits ENOENT (#38780 x #38783). Both reproduce with two-line projects on this head and neither reproduces on main or in the source PRs.
The "stays bisectable" claim does not hold today. 297ed01bf5 (the last folded PR before the fixups) does not compile: src/install/pnpm.rs has the same use crate::package_manager_real::update_package_json_and_install::print_package_json_into_cache_entry; twice and calls &mut lockfile.catalogs on an Option<&mut Lockfile>. The duplicate enters at e2cf8c4305 (#38804) and only leaves in 0adcafaf8b, so at least the last ~40 commits before the fixups do not build.
CI: build 99835 has 92 failed test-bun jobs on every Linux lane plus Windows, about 148 distinct failing assertion sites in ~25 files under test/cli/install; main's 99833 has none. Roughly a third are lockfileVersion 2 snapshots written after #38741, a handful are test-merge errors from the #38504 re-port, and the rest need a run to classify (bun update rows, peer binding in bun-lock.test.ts:2377/2757, minimum-release-age.test.ts:2344, the yarn migration warn lines).
If the goal is to land these, the shape I would push for is: the ~39 PRs whose own CI is green and which touch nothing another folded PR touches go in one by one; the peer-binding/bun.lock-load set, the yarn migrator set, the pnpm migrator set and pack/publish become one stack each with the reconciliation hunks moved into the stack that owns them; #38741, #38797, #38923 and #38813 stay out as their own maintainer-decision PRs. Happy to help cut the stacks.
|
|
||
| // Same target check as the hoisted linker's `install_from_link`. | ||
| let mut link_target: AbsPath = AbsPath::init_top_level_dir(); | ||
| installer.append_store_path(&mut link_target, entry_id); |
There was a problem hiding this comment.
This is a ResolutionTag::Symlink entry, and after #38369 append_store_path hits unreachable!("link: packages have no store path; see append_dependency_path") (Installer.rs:3138) for exactly that tag. Every isolated install with a link: dependency now aborts:
panic: internal error: entered unreachable code: link: packages have no store path; see append_dependency_path
Reproduced on this head with {"dependencies":{"lib":"link:../lib"}} and bun install --linker=isolated; main installs it. It is what takes down isolated-install.test.ts:508/4674/4714/4735/4759, bun-pm-licenses.test.ts:1083 and yarn-lock-migration.test.ts:2357 on every lane. Neither #38045 nor #38369 has this alone. The target check here wants the link target path (append_dependency_path builds it, and returns the ENAMETOOLONG error #38369 added), not the store path.
There was a problem hiding this comment.
Fixed in e4a0624: the symlink-entry check builds its target with installer.append_dependency_path(&mut link_target, entry_id) (which now joins with the ..-normalising checked join, so a link:/file: target that only overflows once joined reports LinkPathTooLong), never append_store_path. {"lib":"link:../lib"} with --linker=isolated installs on the current head; isolated-install.test.ts (111), bun-pm-licenses.test.ts (81) and yarn-lock-migration.test.ts (43) all pass.
| pkg_res: &Resolution, | ||
| ) -> u8 { | ||
| // A global-store entry stays in its staging dir until `Step::Binaries` renames it. | ||
| let mut pkg_dir = AutoAbsPath::init_top_level_dir(); |
There was a problem hiding this comment.
On the relink path nothing was copied into the staging dir, so Which::Staging names a directory that does not exist and get_list fails with ENOENT. Reproduces with isolated-relink.test.ts on this head:
error: failed to fill lifecycle scripts for uses-what-bin: ENOENT
Also behind bun-prune.test.ts:155 and the harness.ts:1578 hits in isolated-install.test.ts. #38780 was written against the pre #38783 layout, where the package was already at its final path when this ran; folded together the assumption in the comment above only holds for a fresh copy.
There was a problem hiding this comment.
Fixed in 0ac90d6: count_blocked_scripts reads from Which::Final unless the entry lives in the global store, so the relink path (nothing staged) reads the package where it already is. isolated-relink, bun-prune and isolated-install pass on the current head.
| /// `libc` recorded in the lockfile, so it does not depend on the host: a | ||
| /// lockfile written on macOS still filters on Linux. | ||
| pub(crate) fn needs_extended_manifest(&self, dependency: Behavior) -> bool { | ||
| self.needs_extended_manifest_to_pick_versions() || dependency.is_optional() |
There was a problem hiding this comment.
This makes every optionalDependencies resolve on every OS refetch the full packument: PackageManifestMap.rs:181-184 marks the cached abbreviated doc unusable when needs_extended is set, and the exact-version fast path is skipped too, so even a pinned optional dep goes back to the network with Accept: application/json and no conditional headers. On macOS and Windows the libc field is never consulted. There is no measurement in #38797. Worth timing a cold bun add vite on main vs this head, and gating on the host actually filtering by libc.
There was a problem hiding this comment.
Confirmed the premise: npm's abbreviated document has no libc (@rollup/rollup-linux-x64-musl corgi keys are cpu, dist, name, os, version; the full doc has libc: ["musl"]), so #38797 has to read the full document to filter at all. Measured the delta for the packages this actually hits: @esbuild/linux-x64 43 KB → 54 KB gzipped, @rollup/rollup-linux-x64-gnu 40 KB → 58 KB. The refetch is one-time per package per manifest cache (the full doc is stored with has_extended_manifest and revalidated with conditional headers afterwards, same path minimumReleaseAge already uses), in exchange for not downloading the other libc's multi-MB tarball on Linux. Gating on the host having a concrete libc would make bun.lock's libc meta depend on which OS resolved the package, which I think is worse than the ~15 KB. I've left #38797 as written and moved it into the "needs a maintainer decision" list in the description next to #38741 rather than change its semantics inside the fold.
| const real = buildTarball(Buffer.from('{"name":"pkg","version":"1.0.0"}\n')); | ||
| const other = buildTarball(Buffer.from('{"name":"other","version":"9.9.9"}\n')); | ||
|
|
||
| await using server = serveManifest(other.sha512, real.tgz); |
There was a problem hiding this comment.
#38484 changed serveManifest to take { integrity, shasum }, but these three calls from #38310 (here, :890, :914) still pass the bare string, so the spread produces {"0":"s","1":"h",...} and the manifest carries no integrity at all. The =1 case then passes for the wrong reason and BUN_CONFIG_NO_VERIFY=0 keeps the integrity check enabled fails on every lane. The runtime side is fine (checked by hand on this head: unset and 0 fail closed on a bad hash, 1 skips), it is the fixture that broke in the merge. Should be serveManifest({ integrity: other.sha512 }, real.tgz).
There was a problem hiding this comment.
Fixed in 4cd0c85: all three calls pass { integrity: other.sha512 }; the =0 case fails closed and =1 skips for the right reason now (tarball-integrity 33/33).
| loaded | ||
| } else { | ||
| Version::V1 | ||
| if !lockfile.overrides.has_scoped() { |
There was a problem hiding this comment.
Flipping new lockfiles back to v1 is a release decision that #38741's own status comment defers to a maintainer, and it obsoletes the open #36464 (its has_scoped && !loaded.at_least(V3) hunk no longer exists) without saying so. It is also the single biggest source of red in 99835: about 15 inline snapshots written by PRs folded after this one still pin "lockfileVersion": 2 (bun-lock.test.ts:1022/2302, catalogs.test.ts:287, overrides.test.ts:247, nested-overrides.test.ts:1481/1513, bun-install-registry.test.ts:1293/1360/6264, migrate.test.ts:2330, minimum-release-age.test.ts:3109, bun-install.test.ts:12035). Either pull this one out and land it or #36464 on its own after the decision, or re-snapshot the siblings inside this commit.
There was a problem hiding this comment.
Agreed this is a release decision, not something the fold should make by accident. The sibling snapshots are already re-stamped to 1 inside the fold (that was fixlv in the fixups), so the branch is internally consistent either way; pulling #38741 back out is one revert plus re-stamping. I've called it out at the top of the description as needing a maintainer decision and noted that it obsoletes #36464.
| @@ -715,7 +731,7 @@ fn print_text(entries: &[Entry], long: bool, checked: usize) { | |||
| "<d>{}<r> {}<d>@{}<r>", | |||
| if last { "└──" } else { "├──" }, | |||
| BStr::new(&entry.name), | |||
| BStr::new(&entry.version) | |||
| bun_core::fmt::EscapeControlChars(redacted(BStr::new(&entry.version))) | |||
There was a problem hiding this comment.
entry.name on the line above is not escaped (the version is). Same drop as why_command.rs:479.
| /// is reported and left as it was rather than failing the command. | ||
| pub fn write_migrated_root(manager: &mut PackageManager) { | ||
| if let Some(root) = take_migrated_root(manager) { | ||
| let _ = write_target(manager, &root); |
There was a problem hiding this comment.
With the write result discarded, bun pm migrate on a read-only package.json exits 0 having written bun.lock but not the workspaces/overrides/catalog it moved out of pnpm-workspace.yaml. The next plain bun install rebuilds those from the package.json it can read (install_with_manager.rs:336-343) and drops the migrated pins without a word. #38754's own test pins the opposite contract (exit 1, no bun.lock). Pick one, and add a test that runs the follow-up bun install and --frozen-lockfile from the half-migrated tree.
There was a problem hiding this comment.
Went with #38754's contract. 9f1c929: write_migrated_root now runs immediately before the migrated lockfile is saved in all four places (both install_with_manager save paths, bun pm migrate, bun pm trust) and exits 1 if the write fails, so a package.json that cannot take the moved fields fails the command before a bun.lock that depends on them exists. The read-only test asserts exit 1, package.json untouched, no bun.lock, no copied … line, and then chmods back and checks the same bun install completes the move and resolves the workspace link. Verified as a non-root user locally.
| bun_sys::FileLockMode::Exclusive, | ||
| /* nonblocking */ true, | ||
| ) { | ||
| let _ = bun_sys::Dir::cwd().delete_tree(tempdir_name); |
There was a problem hiding this comment.
.bunx-lock lives inside tempdir_name, and delete_tree retries on ENOTEMPTY, so a second bun x that just did O_CREAT on a fresh .bunx-lock in the half-deleted directory passes the identity check and then has its lock file unlinked from under it. Two bun x pkg after the 24h expiry can both install "under lock". Putting the lock next to the cache dir instead of inside it removes the race and the identity check.
There was a problem hiding this comment.
9f1c929 moves the lock to <bunx cache dir>.lock (a sibling, so delete_tree never touches it), opens it with O_NOFOLLOW, and on POSIX refuses it unless it is a regular file owned by the current uid; the inode identity re-check and retry loop are gone. bunx / bunx-concurrent pass.
| // The token only travels together with its registry. | ||
| if self.options.did_override_default_scope || !scope.token.is_empty() { | ||
| script_env.put(b"BUN_CONFIG_REGISTRY", scope.url.href())?; | ||
| if !scope.token.is_empty() { |
There was a problem hiding this comment.
Only the default scope's URL and bearer token cross into the nested install. _auth/username:_password basic auth (scope.auth, scope.user), scoped registries, url_auth, ca/cafile, minimumReleaseAge and the security scanner do not, and the child then reads the checkout's own .env/.npmrc/bunfig. So a project on basic auth gets a whole-install failure from the child's 401, and a project with minimumReleaseAge gets devDependencies of the git dep resolved without it. Either forward the install config wholesale or say in the docs exactly what is inherited, with tests for token, _auth, scoped registry and minimumReleaseAge reaching the child.
| @@ -222,6 +228,128 @@ registry = http://localhost:${registry.port}/ | |||
| const result = await publishDryRun(String(dir), { XDG_CONFIG_HOME: "" }); | |||
| expect(result).toEqual(usesRegistry(1)); | |||
| }); | |||
|
|
|||
| // npm's `userconfig` option: the path of the per-user .npmrc, settable through | |||
| // NPM_CONFIG_USERCONFIG like any other npm config option. | |||
There was a problem hiding this comment.
This is #38047 at head 507c356173, the state I requested changes on 2026-08-14. The two tests asked for there are not in the fold: userconfig pointing at a missing file must end in "missing authentication" rather than falling back to ~/.npmrc, and a relative value run from a workspace member dir must resolve against the member dir. Both fit the existing publishDryRun helper.
There was a problem hiding this comment.
Added both in 9f1c929 with the existing publishDryRun helper: NPM_CONFIG_USERCONFIG naming a missing file ends in missing authentication with ~/.npmrc present, and a relative value run from a workspace member resolves against the member dir (root ci.npmrc = registry 1, pkg/ci.npmrc = registry 2, expects 2). Both pass on the folded #38047 code as-is.
…get_or_put_value for ids, tighter helpers (cherry picked from commit 20cf6c9b56b59a5bd0157b279d2420d0d6b0f155)
…orm_list takes the optional field, tuple for the folder-in-dependent case, clippy fix (cherry picked from commit 30671fe6c180a317f8228d78e8aa19a483933f68)
(cherry picked from commit 8233267302fb18a1d9709028a1fe6ce16aaf921b)
(cherry picked from commit 870ea6cecf7518598d82d62d947a3843b09df6e4)
(cherry picked from commit 8494fc54d3afa52f53274df5f143ddba2c7357de)
- yarn.rs: os/cpu fold into Negatable fields at parse time (platform_list gone); dependency sections write straight into entry.dependencies - pnpm.rs: get_object for the pnpm/workspaces lookups, copy_into_root without the get/expect round-trip, root_package_json via entry().ok() - CatalogMap: back to a split borrow (get_or_put_named_group) instead of take-and-restore of the default catalog - npm_lock.rs: keep FolderInDependent as a named struct - PackageJSONEditor: test the catalog tag before the resolution lookup No-Verification-Needed: worktree has no build; install tests run centrally by the integrator (cherry picked from commit a1c29a31366a0e9e3bbb75dc6c4fafe5feed9810)
…edacted(..)) sites
…ontract), bunx lock next to the cache dir with O_NOFOLLOW + owner check, escape names in bun why / pm licenses, userconfig tests for #38047, strings::split lint
|
@alii thanks for pulling this apart. Status against your summary:
|
…too (test), mordant findings (dependency_buf naming, one tag->name helper, prune init without bare bools)
Folds the open
bun install/bun pm/ pack+publish PRs from Aug 12–17 into one branch so they can be reviewed and landed together. Each original PR is one squashed commit here (<title> (#N)), in dependency order, sogit log/git blameattribute every hunk to its PR; conflict resolutions between overlapping PRs live in the later PR's commit. Cross-PR API reconciliation is in themega-merge fixup:commits at the end, so the per-PR commits after #38804 (e2cf8c4305) do not build on their own until0adcafaf8b— bisect across the fold with--first-parenton the fixups, not inside it.171 PRs folded, 3 excluded (see below); #38557 was re-ported by hand onto the newer
audit_command.rs. A simplification pass then roughly halved the netsrc/addition (see below).cargo check --workspaceandcargo clippy --workspaceare clean, the debug build links, and all 94test/cli/install/**files pass locally on Linux (debug+ASAN); see Verification.Not folded
prepareis rarely needed and runs a nested install (fetching and, via prepare, executing third-party devDependencies chosen by the git repo) outside the project's bunfig protections — too much attack surface for the benefitSecurity screen
Every folded diff was reviewed for regressions (path traversal, credential leakage, TLS/integrity weakening, lifecycle-script trust widening, terminal injection, new
unsafeon untrusted input). One PR was dropped for it (#38777 above) and one more on review (#38810). Items a reviewer may still want to eyeball — none were judged exploitable regressions:.bun-tag) no longer apply to any lockfile created after this lands. The reduction is defence-in-depth only: the checks are keyed on a digit the lockfile tamperer also controls, main already stamps v1 whenever bun's own output would trip them, a tamperer can supply a matching integrity for their own tarball anyway, and the git tag is re-validated at checkout/extract time (repository.rs:923, extract_tarball.rs:484; github tags stay checked at every version).?after#leak the fragment into the request path, though no attacker-chosen tarball results.file:targets inside a registry/tarball/git package are now migrated as package-relative folder rows without normalising the sub-path, so a hand-edited key with..is no longer skipped at migration time and containment relies on the installer (hoisted linker checks it, with an overrides-name exemption; isolated linker does not).Product-behaviour changes that need a maintainer decision before this lands: #38741 (new
bun.lockfiles are stampedlockfileVersion: 1again — its own PR defers this to a maintainer, and it obsoletes the open #36464; the sibling snapshots are already re-stamped inside the fold, so pulling it back out is one revert +fixlv), #38797 (libcfiltering needs the full registry document for every optional dependency on every OS, since npm's abbreviated document omitslibc; measured:@esbuild/linux-x6443 KB → 54 KB gzipped,@rollup/rollup-linux-x64-gnu40 KB → 58 KB, one-time per package per manifest cache, in exchange for not downloading the other libc's multi-MB tarballs on Linux and a host-independent lockfile), #38804 (a migrated lockfile and its package.json edits are only written when the command saves a lockfile), #38923 (bun pm lslists what is installed and exits 1 without node_modules), #38813 (pack/publish resolveworkspace:/catalog:from package.json files, not bun.lock).Simplification pass (after folding)
The PRs were written independently, so the folded diff carried duplicate helpers and a lot of narrative comments. A follow-up pass (the
simplify(...)commits) cut thesrc/diff against main from +12,081 / −6,723 (net +5,358) to +10,284 / −7,559 (net +2,725) with no behaviour change intended and the fulltest/cli/install/**suite green before and after. Main items:Lockfile::packages_named/package_satisfying/patched_package_satisfyingreplaces nine "package_index → first candidate whose resolution satisfies a range and a predicate" helpers spread over lockfile.rs, bun.lock.rs, PackageManagerEnqueue.rs, update_transitive.rs and prune.rs.update_transitive.rs(Hold a transitive update that would re-fork a deduped package #38919/install: keep transitive rows on the copy a direct dependency resolves to #38770) no longer re-implements the resolver's rules: it callskeep_locked_if_ahead,is_named_update_row(split out ofshould_update) anddedupe::applied_overridedirectly, and itsPlan/Planned/InstanceEdges/KeepLockedscaffolding is gone; reachability useslockfile::reachable.resolve_path::join_abs_string_buf_z_checked) and one spill helper replacebin.rs::join_z_checked,PackageManager::join_path_z, and pack'snormalize_buf_spill; the bunx install lock (bunx: serialize concurrent installs into the shared cache dir with a file lock #37851) is a singleInstallLockRAII type.append_store_path_at(buf, entry, Which)family;PackageInstall::destination_dir_subpath_bufand theDestinationSubpathguard removed in favour ofjoin_z_spill; staging installs re-target aPackageInstallinstead of threadingdest_subpaththrough every backend;remove_linkshared.Entry::set_git_source, root/workspace package.json parsing shared; pnpm.rs: one merge-basedcopy_into_root, oneworkspacesobject path;finish_migrationbuilds the one error shape.RegistryPath,set_scope_registryand the second credential lookup removed in favour ofscope_for_package_name+UrlAuth::find;fmt::for_terminal(x)=EscapeControlChars(Redacted(x))at every call site;MinimumReleaseAgeExcludesis one list.Two small behaviour fixes fell out of the review agents' cross-checks and are their own commits:
bun audit fixnow rewrites a matching rule inresolutionsas well asoverrides(the parser reads both since #38811), and a failed root package.json write saysfailed to write package.json: <errno>instead offor workspace ''.Conflict resolutions that changed a PR's code (not just context)
*peers after their siblings, not on arrival order #37713 × install: resolve a range onto an existing version only when every install has it #38832: kept install: resolve deferred*peers after their siblings, not on arrival order #37713's rule that a deferred peer never range-matches on sight (no*exemption); install: resolve a range onto an existing version only when every install has it #38832'sAppendedFor/settled_package_countmodel replaces theexact_pinnedguard, with install: keep transitive rows on the copy a direct dependency resolves to #38770's direct-dependency clause folded into the reusable predicate.resolve_peer_dep_version_based_amongkeeps install: rebind ranged peers whose target the saved tree drops #38768's candidate filter but drops the highest-candidate fallback install: keep a peer nothing in bun.lock satisfies where the file records it #38892 removed.collect_bundled_depsrefactor: took install: keep bundleDependencies from every dependency group when parsing a registry manifest #38857's per-versionbundled_dependencieslist.pnpm.rs: oneupdate_package_json_after_migration(lockfile: Option<&mut Lockfile>, …); everything is reported as “copied … in package.json” (pnpm keeps reading its own block), the write is deferred to lockfile save (install: write a migrated lockfile and its package.json edits only when the command saves a lockfile #38804), andbun pm migrateon a read-only package.json reports the failed write but keeps the migrated bun.lock (install: only open package.json for writing in the commands that rewrite it #38745's intent).url_authmodel instead of adding a second.npmrccredential table;--registrygoes through the sameset_default_registrypath unless the URL itself carries credentials (install: send credentials embedded in a registry URL that comes from an env var #38834).EscapeControlChars(redacted(…))(redact inside, escape outside); oneEscapeControlCharsimplementation (the multiline-capable one from pm view: escape control characters coming from the registry #38536) is kept.symlink_dependenciesreturnsResult<bool, TaskError>soLinkPathTooLongstill names the package.fail_fnremoved as in the PR; the invalid-name path from install: reject dependency names containing control characters #38615 now logs like the other root-resolution errors.append_dependency_path, which now joins with the..-normalising checked join, so alink:/file:target that only overflows once joined reportsLinkPathTooLonginstead of hitting the "packages have no store path" unreachable.count_blocked_scriptsreads lifecycle scripts from the final node_modules location for local (non-global-store) entries.bun updateno longer treats a workspace member that was removed fromworkspacesas a direct-dependency owner, so its former deps can move.describe.concurrent).failed to write package.json: EACCESafter the lockfile is saved (test updated).bun update mookeeps a>=0.1.0literal that still covers the new version (main's behaviour); the test asserted^0.2.0.packageDirnow callsetupTest().Folded PRs
Peer binding, hoisting, bun.lock load/round-trip, resolver dedupe (21)
*peers after their siblings, not on arrival order #37713 install: resolve deferred*peers after their siblings, not on arrival orderbun update / bun add package.json edits, overrides (7)
Lockfile migration (yarn / pnpm / npm) (17)
file: / link: / tarball / git local dependencies (16)
Path-buffer overflows: panic → error (15)
Terminal control-character escaping and credential redaction (9)
Registry auth, .npmrc, proxies, URL parsing (16)
bun pm subcommands, audit, help text (14)
bun pm pack / bun publish (13)
Linkers, bins, node_modules layout (9)
Lifecycle scripts, env, minimum-release-age, semver, auto-install (16)
Test-only changes (13)
Platform-specific (5)
Verification
cargo check --workspacecargo clippy --workspace --no-depstest/cli/install/**,test/cli/run/**,test/regression/issue/*install*locally with the debug build. Remaining local-only failures are environmental and reproduce on main in this sandbox:bun-pm.test.tsread-only package.json cases (run as root;nobodycannot exec a binary under/root),thread-spawn-failure(LD_PRELOAD shim vs ASAN).