Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/pm/cli/update.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ Updated packages appear in the install summary as `↑ name old → new`, with `
- A transitive dependency that shares the copy your own `package.json` entry resolves to (in the root or in a workspace) stays on that copy and moves with it. With `@types/node: ~20` in your `package.json`, the `@types/node: *` that `@types/ws` declares keeps using your `~20` copy instead of nesting the newest major under `@types/ws`. Bun updates a transitive range on its own when it rejects the version your entry moves to, or when you removed the entry.
- Bun does not add a version that another one covers. Two dependencies declaring `foo@^1` and `foo@*` share one `1.x` copy, as on `bun install`. Versions already in `bun.lock` and your own `package.json` entries are never moved to save a copy.
- Versions in `patchedDependencies` stay put as long as their range allows. Bun reports them as `kept name@version (patched, v1.2.3 available)`. `--latest` and [`bun audit fix`](/pm/cli/audit#bun-audit-fix) do move them; re-create the patch with [`bun patch`](/pm/cli/patch) afterwards.
- If a registry request for a transitive package fails, that package keeps its locked version and Bun prints a warning. A failed request for a direct dependency is an error.
- An optional dependency on a git repository, a GitHub repository, or a tarball keeps its locked package when Bun cannot fetch that source. Bun prints `warn: name@version was not checked for updates`. This does not apply to a dependency that an [`overrides`](/pm/overrides) rule or a catalog sets. If you pass the `package.json` key of the dependency, or if `--prod` or `--depth 0` selects it, `bun update` exits with code 1 and writes nothing.
- If a registry request for a transitive package fails, that package keeps its locked version and Bun prints a warning. A failed request for a required direct dependency is an error.
- An optional dependency keeps its locked package when Bun cannot reach its source: the registry, a git repository, a GitHub repository, or a tarball. Bun prints `warn: name@version was not checked for updates`. This does not apply to a dependency whose package an [`overrides`](/pm/overrides) rule names, to a dependency that a catalog sets, to a dependency that follows an `npm:` alias of the same name in another `package.json` of the project, or to the dependencies of a package that moves to another version in the same update. If the request for the source fails and you passed the `package.json` key of the dependency, or `--prod` or `--depth 0` selects it, `bun update` exits with code 1 and writes nothing.

## `--interactive`

Expand Down
4 changes: 4 additions & 0 deletions src/install/PackageManager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,9 @@ pub struct PackageManager {
pub(crate) kept_patched: Vec<PackageID>,
pub kept_patched_text: Vec<u8>,

// bun update: the `(name, version)` pairs that update_transitive::warn_unchecked has reported.
pub(crate) warned_unchecked: Vec<(Box<[u8]>, Box<[u8]>)>,

// `Some` while `bun install` resolves. A "no such package" or "no such version" message about a
// dependency that dedupe may still drop is kept here until `log_held` decides whether to log
// it.
Expand Down Expand Up @@ -1726,6 +1729,7 @@ fn allocate_package_manager(
wr!(named_update_reachable, None);
wr!(kept_patched, Vec::new());
wr!(kept_patched_text, Vec::new());
wr!(warned_unchecked, Vec::new());
wr!(held_messages, None);
wr!(manifest_failures, HashMap::default());
wr!(look_ahead, LookAhead::default());
Expand Down
22 changes: 17 additions & 5 deletions src/install/PackageManager/PackageJSONEditor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@ const DEPENDENCY_GROUPS: [DependencyGroup; 4] = [
DependencyGroup::PEER,
];

/// `Lockfile::entries_kept_without_refresh`: package.json entries that stay as written.
pub(crate) type KeptEntries<'a> = &'a [(Option<PackageNameHash>, PackageNameHash)];

#[derive(Default, Clone, Copy)]
pub(crate) struct EditOptions {
pub(crate) struct EditOptions<'a> {
pub exact_versions: bool,
pub before_install: bool,
pub kept: KeptEntries<'a>,
}

#[inline]
Expand Down Expand Up @@ -322,7 +326,7 @@ pub fn edit_trusted_dependencies(
pub(crate) fn edit_update_no_args(
manager: &mut PackageManager,
current_package_json: &mut Expr,
options: EditOptions,
options: EditOptions<'_>,
) -> Result<bool, bun_alloc::AllocError> {
edit_update_entries(
&manager.lockfile,
Expand All @@ -341,7 +345,7 @@ fn edit_update_aliases_of_requests(
manager: &mut PackageManager,
updates: &[UpdateRequest],
current_package_json: &mut Expr,
options: EditOptions,
options: EditOptions<'_>,
) -> Result<bool, bun_alloc::AllocError> {
let is_request = |name: &[u8]| updates.iter().any(|r| r.is_aliased && r.name == name);
let is_alias_of_request = |key: &[u8], literal: &[u8]| {
Expand Down Expand Up @@ -386,7 +390,7 @@ pub(crate) fn edit_update_entries(
workspace_name_hash: Option<PackageNameHash>,
update_to_latest: bool,
current_package_json: &mut Expr,
options: EditOptions,
options: EditOptions<'_>,
selected: &dyn Fn(&[u8], &[u8]) -> bool,
) -> Result<bool, bun_alloc::AllocError> {
// using data store is going to result in undefined memory issues as
Expand Down Expand Up @@ -473,6 +477,10 @@ pub(crate) fn edit_update_entries(
else {
continue;
};
let entry_key = (workspace_name_hash, workspace_dep.name_hash);
if options.kept.contains(&entry_key) {
continue;
}

// an exact pin is not moved by a plain `bun update`
if !update_to_latest
Expand Down Expand Up @@ -976,7 +984,7 @@ pub(crate) fn edit(
updates: &mut &mut [UpdateRequest],
current_package_json: &mut Expr,
dependency_list: &[u8],
options: EditOptions,
options: EditOptions<'_>,
) -> Result<bool, bun_alloc::AllocError> {
// using data store is going to result in undefined memory issues as
// the store is cleared in some workspace situations. the solution
Expand Down Expand Up @@ -1463,6 +1471,10 @@ pub(crate) fn edit(
{
continue;
}
let entry_key = (manager.workspace_name_hash, request.name_hash);
if options.kept.contains(&entry_key) {
continue;
}
if request.package_id as usize >= resolutions.len()
|| resolutions[request.package_id as usize].tag == resolution::Tag::Uninitialized
{
Expand Down
42 changes: 39 additions & 3 deletions src/install/PackageManager/PackageManagerEnqueue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1023,7 +1023,7 @@ pub(crate) fn may_refresh_in_place(dependency: &Dependency) -> bool {
dependency.behavior.is_optional()
&& matches!(
dependency.version.tag,
Tag::Git | Tag::Github | Tag::Tarball
Tag::Npm | Tag::DistTag | Tag::Git | Tag::Github | Tag::Tarball
)
}

Expand All @@ -1045,6 +1045,19 @@ pub(crate) fn refreshes_in_place(
let buf = lockfile.buffers.string_bytes.as_slice();
// An older bun.lock binds a dependency that never resolved to any package of its name.
let is_from_source = match dependency.version.tag {
Tag::Npm => {
let asks = dependency.version.npm();
let literal = dependency.version.literal.slice(buf);
locked_res.tag == ResolutionTag::Npm
&& (asks.name).eql(lockfile.packages.items_name()[locked as usize], buf, buf)
&& Npm::accepts_locked(&asks.version, literal, locked_res.npm().version, buf)
}
// Like a ref of a repository, a tag can point to any version of the package.
Tag::DistTag => {
let asks = dependency.version.dist_tag().name;
locked_res.tag == ResolutionTag::Npm
&& asks.eql(lockfile.packages.items_name()[locked as usize], buf, buf)
}
Tag::Github => {
let asks = dependency.version.github();
locked_res.tag == ResolutionTag::Github
Expand All @@ -1058,7 +1071,7 @@ pub(crate) fn refreshes_in_place(
locked_res.tag == ResolutionTag::Git
&& locked_res.git().repo.slice(buf) == recorded.as_deref().unwrap_or(asks)
}
_ => match &dependency.version.tarball().uri {
Tag::Tarball => match &dependency.version.tarball().uri {
dependency::tarball::Uri::Remote(url) => {
locked_res.tag == ResolutionTag::RemoteTarball
&& locked_res.remote_tarball().eql(*url, buf, buf)
Expand All @@ -1069,6 +1082,7 @@ pub(crate) fn refreshes_in_place(
&& may_name_local_tarball(lockfile, declarer, dependency)
}
},
Tag::Uninitialized | Tag::Folder | Tag::Symlink | Tag::Workspace | Tag::Catalog => false,
};
let has_rule = |name_hash: PackageNameHash| {
lockfile.overrides.has_rule_for_name(name_hash)
Expand Down Expand Up @@ -2021,6 +2035,7 @@ pub(crate) fn bind_row(
let resolve_result = match resolve_result_ {
Ok(v) => v,
Err(err) => {
unbind_refreshing_without_version(this, id, err);
let log_mark = this.log_mut().msgs.len();
report_resolve_error(
this, id, dependency, name, version, err, is_root,
Expand Down Expand Up @@ -2601,6 +2616,24 @@ fn report_manifest_failure(
run_tasks::fail_update_requests(this, task_id, name, None, is_unanswered);
}

/// The manifest has no version for a dependency that is refreshed in place, so its bun.lock package goes. Under `--offline` the manifest is a cached copy that cannot be read again, and the package stays.
#[cold]
fn unbind_refreshing_without_version(
this: &mut PackageManager,
id: DependencyID,
err: crate::Error,
) {
use crate::Error as E;
if this.options.offline != OfflineMode::Offline
&& matches!(
err,
E::NoMatchingVersion | E::DistTagNotFound | E::TooRecentVersion
)
{
this.lockfile.unbind_refreshing(id);
}
Comment thread
robobun marked this conversation as resolved.
Resolved
}

/// Logs why `get_or_put_resolved_package` failed. The caller has to report an `Err` itself.
#[cold]
fn report_resolve_error(
Expand Down Expand Up @@ -3539,7 +3572,10 @@ fn settle_without_manifest(
}
}

if (resolution as usize) < this.lockfile.packages.len() {
// A dependency that is refreshed in place is resolved like one that has no package.
if (resolution as usize) < this.lockfile.packages.len()
&& !this.lockfile.is_refreshing(dependency_id)
{
return Some(Settled::Resolved);
}

Expand Down
2 changes: 1 addition & 1 deletion src/install/PackageManager/add_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -722,7 +722,7 @@ pub(crate) fn edit_target(
updates: &mut &mut [UpdateRequest],
package_json: &mut Expr,
dependency_list: &[u8],
options: EditOptions,
options: EditOptions<'_>,
) -> Result<(), AllocError> {
if !manager.catalog_add.enabled {
PackageJSONEditor::edit(manager, updates, package_json, dependency_list, options)?;
Expand Down
4 changes: 3 additions & 1 deletion src/install/PackageManager/add_remove_with_filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use bun_sys::{Fd, File};
use super::add_catalog;
use super::install_with_manager::install_with_manager;
use super::options::LogLevel;
use super::package_json_editor::EditOptions;
use super::package_json_editor::{EditOptions, KeptEntries};
use super::package_json_write_back;
use super::update_package_json_and_install::{
package_json_entry_or_crash, remove_dependencies_from_package_json,
Expand Down Expand Up @@ -592,6 +592,7 @@ impl PendingWrite {
&mut self,
manager: &mut PackageManager,
updates: &mut [UpdateRequest],
kept_entries: KeptEntries<'_>,
) -> Result<(), Error> {
let dependency_list: &'static [u8] = manager.options.update.prop;
let exact_versions = manager.options.enable.exact_versions();
Expand Down Expand Up @@ -619,6 +620,7 @@ impl PendingWrite {
dependency_list,
EditOptions {
exact_versions,
kept: kept_entries,
..Default::default()
},
);
Expand Down
24 changes: 13 additions & 11 deletions src/install/PackageManager/install_with_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,8 @@ pub fn install_with_manager(
manager.verify_resolutions(log_level);

let mut moved_by_overrides: Vec<PackageID> = Vec::new();
let synced = super::package_json_write_back::edit_after_resolve(manager)?;
let kept = lockfile_before_clean.entries_kept_without_refresh();
let synced = super::package_json_write_back::edit_after_resolve(manager, &kept)?;
if synced.links {
// Removes the registry packages that were resolved for rows that now link a workspace.
manager.lockfile.mark_loaded_packages()?;
Expand Down Expand Up @@ -2318,6 +2319,8 @@ fn refresh_children_of_named(
let pkg_res = lockfile.packages.items_resolution();
latest_rows
.iter()
// A dependency that got no answer is resolved like one that has no package.
.filter(|&&dep_id| !lockfile.is_refreshing(dep_id))
.map(|&dep_id| resolutions[dep_id as usize])
.filter(|&id| {
(id as usize) < pkg_res.len()
Expand Down Expand Up @@ -2491,14 +2494,11 @@ fn rows_outside_overrides(lockfile: &Lockfile) -> Vec<(DependencyID, PackageID)>
version.try_npm().is_some_and(|npm| {
// Under another name the row follows an `npm:` alias, not the rule.
npm.name.slice(buf) == names[package_id as usize].slice(buf)
// An empty range and `*` resolve to `latest`, even if that is a
// prerelease, which no range accepts.
&& !npm.version.is_empty()
&& !crate::npm::is_any_version_range(version.literal.slice(buf))
&& !npm.version.satisfies(
&& !crate::npm::accepts_locked(
&npm.version,
version.literal.slice(buf),
resolutions[package_id as usize].npm().version,
buf,
buf,
)
})
})
Expand All @@ -2516,10 +2516,12 @@ fn is_as_yarn_resolves(lockfile: &Lockfile, row: DependencyID, package_id: Packa
version.try_npm().is_some_and(|npm| {
let resolved = lockfile.packages.items_resolution()[package_id as usize].npm();
npm.name.slice(buf) == lockfile.packages.items_name()[package_id as usize].slice(buf)
// `*` and an empty range resolve to `latest`, which can be a prerelease.
&& (npm.version.is_empty()
|| crate::npm::is_any_version_range(version.literal.slice(buf))
|| npm.version.satisfies(resolved.version, buf, buf))
&& crate::npm::accepts_locked(
&npm.version,
version.literal.slice(buf),
resolved.version,
buf,
)
})
})
}
Expand Down
25 changes: 18 additions & 7 deletions src/install/PackageManager/package_json_write_back.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use super::add_remove_with_filter::{
WorkspaceTarget, fetch_entry, fetch_entry_root, store_entry, write_target,
};
use super::options::Do;
use super::package_json_editor::{self as PackageJSONEditor, Declared, EditOptions};
use super::package_json_editor::{self as PackageJSONEditor, Declared, EditOptions, KeptEntries};
use super::workspace_manifests::same_row;
use super::workspace_package_json_cache::{GetJSONOptions, GetResult, WorkspacePackageJSONCache};
use super::{PackageManager, Subcommand, UpdateRequest};
Expand Down Expand Up @@ -233,7 +233,10 @@ pub(crate) struct Synced {
/// Phase 1, before bun.lock is saved: puts the resolved versions into the in-memory copies of the
/// edited package.json files, which `flush` writes later, and into the lockfile.
#[inline]
pub(crate) fn edit_after_resolve(manager: &mut PackageManager) -> crate::Result<Synced> {
pub(crate) fn edit_after_resolve(
manager: &mut PackageManager,
kept: KeptEntries<'_>,
) -> crate::Result<Synced> {
if manager.pending_filtered_write.is_none()
&& manager.update_target_workspaces.is_none()
&& !manager
Expand All @@ -243,26 +246,29 @@ pub(crate) fn edit_after_resolve(manager: &mut PackageManager) -> crate::Result<
{
return Ok(Synced::default());
}
edit_after_resolve_slow(manager)
edit_after_resolve_slow(manager, kept)
}

#[inline(never)]
fn edit_after_resolve_slow(manager: &mut PackageManager) -> crate::Result<Synced> {
fn edit_after_resolve_slow(
manager: &mut PackageManager,
kept: KeptEntries<'_>,
) -> crate::Result<Synced> {
let mut edited: Vec<EditedPackageJson> = core::mem::take(&mut manager.edited_package_jsons);
let mut updates: Box<[UpdateRequest]> = core::mem::take(&mut manager.update_requests);
let exact = manager.options.enable.exact_versions();
let cwd = edited.iter().position(|e| e.received_requests);

let result = if let Some(mut pending) = manager.pending_filtered_write.take() {
let result = pending.edit_entries(manager, &mut updates);
let result = pending.edit_entries(manager, &mut updates, kept);
manager.pending_filtered_write = Some(pending);
result
} else if let Some(targets) = manager.update_target_workspaces.take() {
let result = edit_update_targets(manager, &targets, &mut edited, exact);
let result = edit_update_targets(manager, &targets, &mut edited, exact, kept);
manager.update_target_workspaces = Some(targets);
result
} else if let Some(i) = cwd {
edit_cwd(manager, &mut edited, i, &mut updates, exact)
edit_cwd(manager, &mut edited, i, &mut updates, exact, kept)
} else {
Ok(())
}
Expand All @@ -286,6 +292,7 @@ fn edit_update_targets(
targets: &[super::UpdateTargetWorkspace],
edited: &mut Vec<EditedPackageJson>,
exact: bool,
kept: KeptEntries<'_>,
) -> crate::Result<()> {
let top_level = FileSystem::instance().top_level_dir();
let mut selected: Vec<WorkspaceTarget> = Vec::new();
Expand Down Expand Up @@ -331,9 +338,11 @@ fn edit_update_targets(
EditOptions {
exact_versions: true,
before_install: true,
..Default::default()
},
EditOptions {
exact_versions: exact,
kept,
..Default::default()
},
] {
Expand Down Expand Up @@ -370,11 +379,13 @@ fn edit_cwd(
cwd_index: usize,
updates: &mut [UpdateRequest],
exact: bool,
kept: KeptEntries<'_>,
) -> crate::Result<()> {
let cwd_target = edited[cwd_index].target.clone();
let in_root = cwd_target.name_hash.is_none();
let options = EditOptions {
exact_versions: exact,
kept,
..Default::default()
};
let mut ast = fetch_entry_root(manager, &cwd_target);
Expand Down
Loading
Loading