Skip to content
Closed
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
2 changes: 1 addition & 1 deletion completions/bun.bash
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ _bun_completions() {
PACKAGE_OPTIONS[ADD_OPTIONS_SHORT]="-d -F";
PACKAGE_OPTIONS[REMOVE_OPTIONS_LONG]="--filter";
PACKAGE_OPTIONS[REMOVE_OPTIONS_SHORT]="-F";
PACKAGE_OPTIONS[UPDATE_OPTIONS_LONG]="--latest --interactive --recursive --filter --dev --development --prod --no-optional --exact";
PACKAGE_OPTIONS[UPDATE_OPTIONS_LONG]="--latest --interactive --recursive --filter --dev --development --prod --no-optional --depth --exact";
PACKAGE_OPTIONS[UPDATE_OPTIONS_SHORT]="-L -i -r -F -d -D -P -E";

PACKAGE_OPTIONS[SHARED_OPTIONS_LONG]="--config --yarn --production --frozen-lockfile --no-save --dry-run --force --cache-dir --no-cache --silent --verbose --global --cwd --backend --link-native-bins --help";
Expand Down
1 change: 1 addition & 0 deletions completions/bun.fish
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ complete -c bun -n "__fish_seen_subcommand_from update" -s "P" -l "prod" -d "Onl
complete -c bun -n "__fish_seen_subcommand_from update" -s "d" -l "dev" -d "Only update devDependencies" -f
complete -c bun -n "__fish_seen_subcommand_from update" -s "D" -l "development" -d "Only update devDependencies" -f
complete -c bun -n "__fish_seen_subcommand_from update" -l "no-optional" -d "Don't update optionalDependencies" -f
complete -c bun -n "__fish_seen_subcommand_from update" -l "depth" -d "Only 0 is accepted: update direct dependencies and keep transitive dependencies locked" -x -a "0"
complete -c bun -n "__fish_seen_subcommand_from update" -s "E" -l "exact" -d "Write exact versions to package.json instead of ^ or ~ ranges" -f
complete -c bun -n "__fish_seen_subcommand_from update" -s "L" -l "latest" -d "Update packages to their latest versions, ignoring the ranges in package.json" -f
complete -c bun -n "__fish_seen_subcommand_from update" -s "i" -l "interactive" -d "Show an interactive list of outdated packages to select for update" -f
Expand Down
1 change: 1 addition & 0 deletions completions/bun.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,7 @@ _bun_update_completion() {
'-D[Only update devDependencies]' \
'--development[Only update devDependencies]' \
'--no-optional[Don'"'"'t update optionalDependencies]' \
'--depth[Only 0 is accepted: update direct dependencies and keep transitive dependencies locked]:depth:(0)' \
'-E[Write exact versions to package.json instead of ^ or ~ ranges]' \
'--exact[Write exact versions to package.json instead of ^ or ~ ranges]' \
'--no-save[Don'"'"'t save a lockfile]' \
Expand Down
12 changes: 12 additions & 0 deletions docs/pm/cli/update.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,18 @@ bun update -D '@types/*'
bun update -i --prod
```

## `--depth 0`

By default, `bun update` also moves transitive dependencies to the newest version their dependents' ranges allow. `--depth 0` updates only the `package.json` entries of the workspace you run it in and keeps every transitive dependency at the version recorded in `bun.lock`. This matches `pnpm update --depth 0`. A transitive dependency still moves when the updated direct dependency declares a range that its locked version no longer satisfies.

It combines with names, patterns, `--dev`, `--prod`, `--no-optional`, `--latest`, `--recursive`, and `--filter`. Only `0` is accepted as a depth.

```sh terminal icon="terminal"
bun update --depth 0
bun update --depth 0 --latest
bun update --depth 0 -r
```

## `--global`

`bun update -g` updates packages installed with `bun add -g`:
Expand Down
4 changes: 4 additions & 0 deletions docs/snippets/cli/update.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ bun up
Don't update <code>optionalDependencies</code>
</ParamField>

<ParamField path="--depth" type="number">
Only <code>0</code> is accepted: update direct dependencies and keep transitive dependencies at their locked versions
</ParamField>

<ParamField path="--global" type="boolean">
Install globally. Alias: <code>-g</code>
</ParamField>
Expand Down
19 changes: 19 additions & 0 deletions src/install/PackageManager/CommandLineArguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ pub(crate) static UPDATE_PARAMS: &[ParamType] = concat_params![
clap::param!("-d, --dev Only update devDependencies"),
clap::param!("-D, --development"),
clap::param!("--no-optional Don't update optionalDependencies"),
clap::param!(
"--depth <NUM> Only 0 is accepted: update direct dependencies and keep transitive dependencies at their locked versions"
),
Comment thread
robobun marked this conversation as resolved.
Resolved
clap::param!(
"-E, --exact Write exact versions to package.json instead of ^ or ~ ranges"
),
Expand Down Expand Up @@ -537,6 +540,8 @@ pub struct CommandLineArguments {
pub(crate) recursive: bool,
pub(crate) filters: &'static [&'static [u8]],
pub update_groups: UpdateGroups,
/// `bun update --depth 0`: transitive rows keep their lockfile resolution.
pub(crate) update_direct_only: bool,

pub(crate) pack_destination: &'static [u8],
pub(crate) pack_filename: &'static [u8],
Expand Down Expand Up @@ -641,6 +646,7 @@ impl Default for CommandLineArguments {
recursive: false,
filters: &[],
update_groups: UpdateGroups::default(),
update_direct_only: false,

pack_destination: b"",
pack_filename: b"",
Expand Down Expand Up @@ -827,6 +833,9 @@ Full documentation is available at <magenta>https://bun.com/docs/cli/install<r>.
<d>Only update dependencies and optionalDependencies:<r>
<b><green>bun update<r> <cyan>--prod<r>

<d>Only update direct dependencies, keep transitive dependencies locked:<r>
<b><green>bun update<r> <cyan>--depth 0<r>

Full documentation is available at <magenta>https://bun.com/docs/cli/update<r>.
";
pretty_help(intro_text);
Expand Down Expand Up @@ -1648,6 +1657,16 @@ Full documentation is available at <magenta>https://bun.com/docs/pm/cli/prune<r>
no_optional: args.flag(b"--no-optional"),
};
cli.production = false;
if let Some(depth) = args.option(b"--depth") {
if depth != b"0" {
Output::err_generic(
"bun update --depth only accepts 0 (update direct dependencies, keep transitive dependencies locked), got '{}'",
(bstr::BStr::new(depth),),
);
Global::exit(1);
}
cli.update_direct_only = true;
}
}

let specified_backend: Option<package_install::Method> = 'brk: {
Expand Down
10 changes: 8 additions & 2 deletions src/install/PackageManager/PackageManagerEnqueue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2306,8 +2306,14 @@ fn get_or_put_resolved_package_with_find_result(
(this.is_update_request(dependency.name_hash, dependency.name.slice(string_buf))
|| (name_hash != dependency.name_hash
&& this.is_update_request(name_hash, name.slice(string_buf))))
&& crate::update_scope::UpdateScope::of(&*this)
.contains_dependency(&this.lockfile, dependency_id)
&& {
let scope = crate::update_scope::UpdateScope::of(&*this);
if this.options.do_.update_direct_only() {
scope.contains_direct_dependency(&this.lockfile, dependency_id)
} else {
scope.contains_dependency(&this.lockfile, dependency_id)
}
}
} else if let Some(targets) = this.update_target_workspaces.as_deref() {
// `bun update -r`/`--filter`: direct deps of the selected workspaces; catalogs are root-scoped.
dependency.version.tag == dependency::version::Tag::Catalog
Expand Down
9 changes: 8 additions & 1 deletion src/install/PackageManager/PackageManagerOptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,7 @@ impl Options {

self.do_.set(Do::UPDATE_TO_LATEST, cli.latest);
self.do_.set(Do::RECURSIVE, cli.recursive);
self.do_.set(Do::UPDATE_DIRECT_ONLY, cli.update_direct_only);

if !cli.positionals.is_empty() {
self.positionals = cli.positionals;
Expand Down Expand Up @@ -965,7 +966,9 @@ bitflags::bitflags! {
const ANALYZE = 1 << 11;
const RECURSIVE = 1 << 12;
const PREFETCH_RESOLVED_TARBALLS = 1 << 13;
// _: u2 padding
/// `bun update --depth 0`: only root and workspace rows re-resolve.
const UPDATE_DIRECT_ONLY = 1 << 14;
// _: u1 padding
}
}

Expand Down Expand Up @@ -1059,6 +1062,10 @@ impl Do {
pub fn recursive(self) -> bool {
self.contains(Do::RECURSIVE)
}
#[inline]
pub fn update_direct_only(self) -> bool {
self.contains(Do::UPDATE_DIRECT_ONLY)
}
}

// Field-style accessors (`options.enable.cache = false` /
Expand Down
17 changes: 13 additions & 4 deletions src/install/PackageManager/install_with_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -649,9 +649,11 @@ pub fn install_with_manager(
resolve_pending_tasks(manager, &root, log_level, &mut named)?;
}

direct_deps_before.redirect_dependents(&mut manager.lockfile);
// `--depth 0`: a transitive edge that shared the old package with a moved direct entry stays on it.
let direct_only = manager.options.do_.update_direct_only();
direct_deps_before.redirect_dependents(&mut manager.lockfile, direct_only);
transitive.redirect_dependents(&mut manager.lockfile);
redirect_moved_edges(&mut manager.lockfile, &named.moved);
redirect_moved_edges(&mut manager.lockfile, &named.moved, direct_only);
transitive.print_plan(manager, &direct_deps_before, &named.moved);
print_kept_patched(manager);

Expand Down Expand Up @@ -1571,9 +1573,16 @@ fn enqueue_named_updates(
direct: &DirectDependencies,
lockfile_name: &'static str,
) -> crate::Result<NamedUpdates> {
let walkable = crate::update_scope::UpdateScope::of(&*manager).walkable_rows(&manager.lockfile);
let direct_only = manager.options.do_.update_direct_only();
let scope = crate::update_scope::UpdateScope::of(&*manager);
let walkable = if direct_only {
scope.direct_walkable_rows(&manager.lockfile)
} else {
scope.walkable_rows(&manager.lockfile)
};
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
let plannable_peers = plannable_peer_rows(&manager.lockfile, direct);
let collect_latest_rows = manager.options.do_.update_to_latest();
// `--depth 0` keeps the children of a moved package at their locked versions, so there is nothing to refresh.
let collect_latest_rows = manager.options.do_.update_to_latest() && !direct_only;
Comment thread
robobun marked this conversation as resolved.
Resolved
let requests = manager.update_requests.len();
let mut matched = DynamicBitSet::init_empty(requests).unwrap_or_oom();
let mut matched_elsewhere = DynamicBitSet::init_empty(requests).unwrap_or_oom();
Expand Down
85 changes: 78 additions & 7 deletions src/install/update_scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,50 @@ impl UpdateScope<'_> {
}
}

/// `bun update --depth 0`: only a row the root or a workspace owns may move; a moved package's own rows keep whatever bun.lock already resolves for them.
pub fn contains_direct_dependency(&self, lockfile: &Lockfile, dep_id: DependencyID) -> bool {
match lockfile
.packages
.items_dependencies()
.iter()
.position(|slice| slice.contains(dep_id))
{
Some(owner) => {
matches!(
lockfile.packages.items_resolution()[owner].tag,
ResolutionTag::Root | ResolutionTag::Workspace
) && self.contains_package(lockfile, owner)
}
None => true,
}
}

/// One bit per dependency row; rows covered by no package's slice (orphans left by the differ) stay unset.
pub fn walkable_rows(&self, lockfile: &Lockfile) -> DynamicBitSet {
self.walkable_rows_of(lockfile, false)
}

/// `bun update --depth 0`: only the rows the root and the workspaces own, so a transitive row that shares a name with a direct dependency stays locked.
pub fn direct_walkable_rows(&self, lockfile: &Lockfile) -> DynamicBitSet {
self.walkable_rows_of(lockfile, true)
}

fn walkable_rows_of(&self, lockfile: &Lockfile, direct_only: bool) -> DynamicBitSet {
let mut walk =
DynamicBitSet::init_empty(lockfile.buffers.dependencies.len()).unwrap_or_oom();
let pkg_res = lockfile.packages.items_resolution();
for (id, slice) in lockfile.packages.items_dependencies().iter().enumerate() {
if slice.len == 0 {
continue;
}
if direct_only
&& !matches!(
pkg_res[id].tag,
ResolutionTag::Root | ResolutionTag::Workspace
)
{
continue;
}
if self.contains_package(lockfile, id) {
walk.set_range_value(
Range {
Expand All @@ -180,6 +216,30 @@ impl UpdateScope<'_> {
}
}

/// One bit per dependency row the root or a workspace owns.
pub fn direct_rows(lockfile: &Lockfile) -> DynamicBitSet {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
let mut rows = DynamicBitSet::init_empty(lockfile.buffers.dependencies.len()).unwrap_or_oom();
let pkg_res = lockfile.packages.items_resolution();
for (id, slice) in lockfile.packages.items_dependencies().iter().enumerate() {
if slice.len == 0
|| !matches!(
pkg_res[id].tag,
ResolutionTag::Root | ResolutionTag::Workspace
)
{
continue;
}
rows.set_range_value(
Range {
start: slice.begin() as usize,
end: slice.end() as usize,
},
true,
);
}
rows
}

/// Which package.json entries --dev / --prod / --no-optional cover; peer entries are covered only when no selector is given.
pub fn selects(groups: UpdateGroups, behavior: Behavior) -> bool {
if groups.no_optional && behavior.is_optional() {
Expand Down Expand Up @@ -250,12 +310,13 @@ fn matches(glob: &[u8], name: &[u8]) -> bool {
}
}

fn describe_groups(groups: UpdateGroups) -> Vec<u8> {
fn describe_selectors(groups: UpdateGroups, direct_only: bool) -> Vec<u8> {
let mut out: Vec<u8> = Vec::new();
for (on, flag) in [
(groups.dev, &b"--dev"[..]),
(groups.prod, b"--prod"),
(groups.no_optional, b"--no-optional"),
(direct_only, b"--depth 0"),
] {
if on {
if !out.is_empty() {
Expand Down Expand Up @@ -322,11 +383,13 @@ fn exit_on_lockfile_load_failure(manager: &mut PackageManager, subject: &[u8]) -
}
}

/// Turns `bun update` patterns and `--dev`/`--prod`/`--no-optional` into the concrete names the named path expects; a plain `bun update [name]` returns before doing anything.
/// Turns `bun update` patterns and `--dev`/`--prod`/`--no-optional`/`--depth 0` into the concrete names the named path expects; a plain `bun update [name]` returns before doing anything.
pub fn expand_positionals(manager: &mut PackageManager, original_cwd: &[u8], groups: UpdateGroups) {
let positionals = manager.options.positionals;
let args = positionals.get(1..).unwrap_or(&[]);
let selecting = !groups.is_default();
// `--depth 0` selects every group but walks only the root and workspace rows, like the group selectors do.
let direct_only = manager.options.do_.update_direct_only();
let selecting = !groups.is_default() || direct_only;
Comment thread
robobun marked this conversation as resolved.
Resolved
if !selecting && !args.iter().any(|a| is_pattern(a)) {
return;
}
Expand All @@ -337,7 +400,7 @@ pub fn expand_positionals(manager: &mut PackageManager, original_cwd: &[u8], gro
if selecting {
if has_version_suffix(arg) {
Output::err_generic(
"a version cannot be combined with --dev, --prod or --no-optional: {}",
"a version cannot be combined with --dev, --prod, --no-optional or --depth 0: {}",
(BStr::new(arg),),
);
Global::exit(1);
Expand All @@ -358,7 +421,7 @@ pub fn expand_positionals(manager: &mut PackageManager, original_cwd: &[u8], gro
}

let subject = if patterns.is_empty() {
describe_groups(groups)
describe_selectors(groups, direct_only)
} else {
describe_patterns(&patterns)
};
Expand Down Expand Up @@ -483,11 +546,16 @@ pub fn expand_positionals(manager: &mut PackageManager, original_cwd: &[u8], gro
let mut failed = false;
for pattern in patterns.iter().filter(|p| !p.negated && !p.hit) {
failed = true;
if selecting {
if !groups.is_default() {
Output::err_generic(
"no dependencies in the selected groups match \"{}\"",
(BStr::new(pattern.raw),),
);
} else if direct_only {
Output::err_generic(
"no direct dependencies match \"{}\"",
(BStr::new(pattern.raw),),
);
} else {
Output::err_generic(
"no packages in {} match \"{}\"",
Expand All @@ -506,7 +574,10 @@ pub fn expand_positionals(manager: &mut PackageManager, original_cwd: &[u8], gro
if checked == 1 { "y" } else { "ies" }
);
if selecting {
pretty!("selected by {}", BStr::new(&describe_groups(groups)));
pretty!(
"selected by {}",
BStr::new(&describe_selectors(groups, direct_only))
);
}
if !patterns.is_empty() {
pretty!(
Expand Down
Loading
Loading