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
2 changes: 1 addition & 1 deletion completions/bun-cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -3368,7 +3368,7 @@
},
"ls": {
"name": "ls",
"description": "list the dependency tree according to the current lockfile",
"description": "list the tree of installed dependencies",
"flags": [],
"positionalArgs": []
},
Expand Down
4 changes: 2 additions & 2 deletions completions/bun.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ _bun_pm_completion() {
cmd2)
sub_commands=(
'bin\:"print the path to bin folder" '
'ls\:"list the dependency tree according to the current lockfile" '
'ls\:"list the tree of installed dependencies" '
'licenses\:"list installed packages grouped by license" '
'hash\:"generate & print the hash of the current lockfile" '
'hash-string\:"print the string used to hash the lockfile" '
Expand Down Expand Up @@ -295,7 +295,7 @@ _bun_pm_completion() {
;;
ls)
pmargs=(
"--all[list the entire dependency tree according to the current lockfile]"
"--all[list the entire tree of installed dependencies]"
"--trusted[list only trusted dependencies]"
)

Expand Down
4 changes: 2 additions & 2 deletions src/install/PackageManager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@ impl PackageManagerCommand {
<d>└<r> <cyan>--quiet<r> only output the tarball filename
<b><green>bun pm<r> <blue>bin<r> print the path to bin folder
<d>└<r> <cyan>-g<r> print the <b>global<r> path to bin folder
<b><green>bun pm<r> <blue>ls<r> list the dependency tree according to the current lockfile
<d>├<r> <cyan>--all<r> list the entire dependency tree according to the current lockfile
<b><green>bun pm<r> <blue>ls<r> list the tree of installed dependencies
<d>├<r> <cyan>--all<r> list the entire tree of installed dependencies
<d>└<r> <cyan>--trusted<r> list only trusted dependencies
<b><green>bun pm<r> <blue>why<r> <d>\<pkg\><r> show dependency tree explaining why a package is installed
<b><green>bun pm<r> <blue>licenses<r> list installed packages grouped by license
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2260,7 +2260,7 @@ Full documentation is available at <magenta>https://bun.com/docs/cli/why<r>

<b>Commands:<r>
<b><green>bun pm<r> <blue>bin<r> print the path to bin folder
<b><green>bun pm<r> <blue>ls<r> list the dependency tree according to the current lockfile
<b><green>bun pm<r> <blue>ls<r> list the tree of installed dependencies
<b><green>bun pm<r> <blue>whoami<r> print the current npm username
<b><green>bun pm<r> <blue>hash<r> generate & print the hash of the current lockfile
<b><green>bun pm<r> <blue>cache<r> print the path to the cache folder
Expand Down
117 changes: 110 additions & 7 deletions src/runtime/cli/package_manager_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ use bun_install::package_manager_real::{
CommandLineArguments, Subcommand, fetch_cache_directory_path, get_cache_directory,
package_manager_options::LogLevel, setup_global_dir,
};
use bun_install::{DependencyID, PackageID, PackageManager, migration};
use bun_paths::{self as Path, PathBuffer};
use bun_install::{DependencyID, PackageID, PackageManager, ResolutionTag, migration};
use bun_paths::{self as Path, AutoAbsPath, PathBuffer};
use bun_resolver::fs as Fs;
use bun_sys::{self, Dir, Fd, File};

use crate::cli::Command;
use crate::cli::pm_diff_command as PmDiffCommand;
use crate::cli::pm_licenses_command::{LicensesFlags, PmLicensesCommand};
use crate::cli::pm_licenses_command::{BunStore, LicensesFlags, PmLicensesCommand};
use crate::cli::pm_pkg_command::PmPkgCommand;
use crate::cli::pm_trusted_command::{DefaultTrustedCommand, TrustCommand, UntrustedCommand};
use crate::cli::pm_version_command::PmVersionCommand;
Expand All @@ -38,6 +38,83 @@ pub(crate) struct NodeModulesFolder {
dependencies: Box<[DependencyID]>,
}

/// Disk-existence check for `bun pm ls`: the lockfile also contains entries
/// that were never installed (platform-mismatched optionals) or were removed
/// after install (`bun prune --production`), and those must not be listed as
/// the contents of `node_modules`.
Comment thread
robobun marked this conversation as resolved.
struct InstalledFilter {
path: AutoAbsPath,
top_len: usize,
store: BunStore,
}

impl InstalledFilter {
fn init() -> Self {
let path = AutoAbsPath::init_top_level_dir();
let top_len = path.len();
Self {
path,
top_len,
store: BunStore::init(),
}
}

fn node_modules_exists(&mut self) -> bool {
self.path.set_length(self.top_len);
let _ = self.path.append(b"node_modules");
let exists = bun_sys::exists(self.path.slice());
self.path.set_length(self.top_len);
exists
}

fn is_installed(
&mut self,
lockfile: &Lockfile,
relative_path: &[u8],
dep_id: DependencyID,
) -> bool {
let package_id = lockfile.buffers.resolutions.as_slice()[dep_id as usize];
if package_id as usize >= lockfile.packages.len() {
return false;
}
let slice = lockfile.packages.slice();
let resolution = &slice.items_resolution()[package_id as usize];
// Local sources (the project itself, workspaces, `bun link`ed packages)
// always exist on disk outside of node_modules.
Comment thread
robobun marked this conversation as resolved.
if matches!(
resolution.tag,
ResolutionTag::Root | ResolutionTag::Workspace | ResolutionTag::Symlink
) {
return true;
}

let buf = lockfile.buffers.string_bytes.as_slice();
let alias = lockfile.buffers.dependencies.as_slice()[dep_id as usize]
.name
.slice(buf);
self.path.set_length(self.top_len);
let _ = self.path.append(relative_path);
let _ = self.path.append(alias);
let exists = bun_sys::exists(self.path.slice());
self.path.set_length(self.top_len);
if exists {
return true;
}

// Isolated installs (`node_modules/.bun`) only link direct dependencies
// into each package's node_modules; transitive packages live in the store.
Comment thread
robobun marked this conversation as resolved.
self.store
.lookup(
&mut self.path,
self.top_len,
slice.items_name()[package_id as usize],
resolution,
buf,
)
.is_some()
}
}

// Transient sort-comparator context; lifetime is fn-local.
struct ByName<'a> {
dependencies: &'a [Dependency],
Expand Down Expand Up @@ -186,8 +263,8 @@ impl PackageManagerCommand {
<d>└<r> <cyan>--quiet<r> only output the tarball filename\n\
<b><green>bun pm<r> <blue>bin<r> print the path to bin folder\n\
<d>└<r> <cyan>-g<r> print the <b>global<r> path to bin folder\n\
<b><green>bun pm<r> <blue>ls<r> list the dependency tree according to the current lockfile\n\
<d>├<r> <cyan>--all<r> list the entire dependency tree according to the current lockfile\n\
<b><green>bun pm<r> <blue>ls<r> list the tree of installed dependencies\n\
<d>├<r> <cyan>--all<r> list the entire tree of installed dependencies\n\
<d>└<r> <cyan>--trusted<r> list only trusted dependencies\n\
<b><green>bun pm<r> <blue>why<r> <d>\\<pkg\\><r> show dependency tree explaining why a package is installed\n\
<b><green>bun pm<r> <blue>diff<r> <d>[a] [b]<r> show what changed between two versions of a package (or vs a folder/tarball)\n\
Expand Down Expand Up @@ -563,10 +640,21 @@ Learn more about these at <magenta>https://bun.com/docs/cli/pm<r>.\n";
Output::flush();
Output::disable_buffering();
let lockfile: &Lockfile = &pm.lockfile;

let mut installed = InstalledFilter::init();
if !installed.node_modules_exists() {
if log_level != LogLevel::Silent {
Output::err_generic("node_modules not found, nothing to list", ());
bun_core::note!("run 'bun install' first");
}
Global::exit(1);
}

let mut iterator =
tree::Iterator::<{ tree::IteratorPathStyle::NodeModules }>::init(lockfile);

let mut max_depth: usize = 0;
let mut installed_count: usize = 0;

let mut directories: Vec<NodeModulesFolder> = Vec::new();
while let Some(node_modules) = iterator.next(None) {
Expand All @@ -575,7 +663,19 @@ Learn more about these at <magenta>https://bun.com/docs/cli/pm<r>.\n";
path.extend_from_slice(node_modules.relative_path.as_bytes());
path.push(0);

let dependencies: Box<[DependencyID]> = Box::from(node_modules.dependencies);
let dependencies: Box<[DependencyID]> = node_modules
.dependencies
.iter()
.copied()
.filter(|&dep_id| {
installed.is_installed(
lockfile,
node_modules.relative_path.as_bytes(),
dep_id,
)
})
.collect();
installed_count += dependencies.len();

if max_depth < node_modules.depth + 1 {
max_depth = node_modules.depth + 1;
Expand Down Expand Up @@ -638,7 +738,7 @@ Learn more about these at <magenta>https://bun.com/docs/cli/pm<r>.\n";
Output::println(format_args!(
"{} node_modules ({})",
bstr::BStr::new(path),
lockfile.buffers.hoisted_dependencies.len(),
installed_count,
));
let string_bytes = lockfile.buffers.string_bytes.as_slice();
let mut sorted_dependencies: Vec<DependencyID> =
Expand All @@ -654,6 +754,9 @@ Learn more about these at <magenta>https://bun.com/docs/cli/pm<r>.\n";
index_sort::sort_indices(&mut sorted_dependencies, &mut |a, b| by_name.cmp(a, b));
sorted_dependencies.dedup_by(|a, b| by_name.cmp(*a, *b) == Ordering::Equal);

sorted_dependencies
.retain(|&dep_id| installed.is_installed(lockfile, b"node_modules", dep_id));

if trusted_only {
sorted_dependencies.retain(|&dep_id| {
let package_id = lockfile.buffers.resolutions.as_slice()[dep_id as usize];
Expand Down
10 changes: 7 additions & 3 deletions src/runtime/cli/pm_licenses_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ struct Entry {
}

/// Lazily-scanned, sorted entry names of `node_modules/.bun/`; `None` until first needed.
struct BunStore {
pub(crate) struct BunStore {
entries: Option<Vec<Box<[u8]>>>,
}

Expand Down Expand Up @@ -197,7 +197,7 @@ impl PmLicensesCommand {
let buf = lockfile.buffers.string_bytes.as_slice();

let mut log = Log::init();
let mut store = BunStore { entries: None };
let mut store = BunStore::init();
let mut disk = DiskIndex { entries: None };
let mut entries: Vec<Entry> = Vec::new();
let mut missing: usize = 0;
Expand Down Expand Up @@ -523,6 +523,10 @@ fn author_of(json: &Expr) -> Option<Box<[u8]>> {
}

impl BunStore {
pub(crate) fn init() -> Self {
Self { entries: None }
}

fn read_info(
&mut self,
path: &mut AutoAbsPath,
Expand All @@ -543,7 +547,7 @@ impl BunStore {
read_package_info_at(path, top_len, &segments, log)
}

fn lookup(
pub(crate) fn lookup(
&mut self,
path: &mut AutoAbsPath,
top_len: usize,
Expand Down
2 changes: 1 addition & 1 deletion test/cli/bun.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ describe("bun", () => {
"bun pm --help",
["pm"],
[
/^ {2}bun pm ls +list the dependency tree according to the current lockfile$/m,
/^ {2}bun pm ls +list the tree of installed dependencies$/m,
/^ {2}bun pm licenses +list installed packages grouped by license$/m,
],
],
Expand Down
Loading
Loading