Skip to content

Commit 512d919

Browse files
authored
fix(install): cleanup node_modules folder on install (#32058)
This is only for `deno install/add/remove`. We don't do this for other commands because it could cause problems (ex. two deno subprocesses running at the same time with "lazy npm deps" or whatever it's called) and also it would add more startup time.
1 parent 652d4d2 commit 512d919

File tree

31 files changed

+619
-557
lines changed

31 files changed

+619
-557
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ simd-json = "0.14.0"
264264
slab = "0.4"
265265
smallvec = "1.8"
266266
socket2 = { version = "0.5.3", features = ["all"] }
267-
sys_traits = "=0.1.22"
267+
sys_traits = "=0.1.24"
268268
tar = "=0.4.43"
269269
tempfile = "3.4.0"
270270
termcolor = "1.1.3"

cli/factory.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ use crate::args::ConfigFlag;
6767
use crate::args::DenoSubcommand;
6868
use crate::args::Flags;
6969
use crate::args::InstallFlags;
70+
use crate::args::InstallFlagsLocal;
7071
use crate::cache::Caches;
7172
use crate::cache::CodeCache;
7273
use crate::cache::DenoDir;
@@ -600,6 +601,54 @@ impl CliFactory {
600601
.cloned()
601602
.map(|r| r as Arc<dyn deno_npm_installer::InstallReporter>),
602603
NpmInstallerFactoryOptions {
604+
clean_on_install: match cli_options.sub_command() {
605+
DenoSubcommand::Add { .. }
606+
| DenoSubcommand::ApproveScripts { .. }
607+
| DenoSubcommand::Remove { .. }
608+
| DenoSubcommand::Cache { .. }
609+
| DenoSubcommand::Uninstall { .. } => true,
610+
DenoSubcommand::Install(flags) => match flags {
611+
InstallFlags::Local(flags) => match flags {
612+
InstallFlagsLocal::Add { .. }
613+
| InstallFlagsLocal::TopLevel { .. } => true,
614+
// someone might be terribly storing dependencies in a
615+
// module and needs to populate their node_modules directory
616+
// with the dependencies in that file
617+
InstallFlagsLocal::Entrypoints { .. } => false,
618+
},
619+
InstallFlags::Global(_) => false,
620+
},
621+
DenoSubcommand::Audit { .. }
622+
| DenoSubcommand::Bench { .. }
623+
| DenoSubcommand::Bundle { .. }
624+
| DenoSubcommand::Check { .. }
625+
| DenoSubcommand::Clean { .. }
626+
| DenoSubcommand::Compile { .. }
627+
| DenoSubcommand::Completions { .. }
628+
| DenoSubcommand::Coverage { .. }
629+
| DenoSubcommand::Deploy { .. }
630+
| DenoSubcommand::Doc { .. }
631+
| DenoSubcommand::Eval { .. }
632+
| DenoSubcommand::Fmt { .. }
633+
| DenoSubcommand::Init { .. }
634+
| DenoSubcommand::Info { .. }
635+
| DenoSubcommand::JSONReference { .. }
636+
| DenoSubcommand::Jupyter { .. }
637+
| DenoSubcommand::Lsp
638+
| DenoSubcommand::Lint { .. }
639+
| DenoSubcommand::Repl { .. }
640+
| DenoSubcommand::Run { .. }
641+
| DenoSubcommand::Serve { .. }
642+
| DenoSubcommand::Task { .. }
643+
| DenoSubcommand::Test { .. }
644+
| DenoSubcommand::Outdated { .. }
645+
| DenoSubcommand::Types
646+
| DenoSubcommand::Upgrade { .. }
647+
| DenoSubcommand::Vendor
648+
| DenoSubcommand::Publish { .. }
649+
| DenoSubcommand::Help { .. }
650+
| DenoSubcommand::X { .. } => false,
651+
},
603652
cache_setting: NpmCacheSetting::from_cache_setting(
604653
&cli_options.cache_setting(),
605654
),

cli/lsp/config.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,6 +1487,7 @@ impl ConfigData {
14871487
pb,
14881488
None,
14891489
NpmInstallerFactoryOptions {
1490+
clean_on_install: false,
14901491
cache_setting: NpmCacheSetting::Use,
14911492
caching_strategy: NpmCachingStrategy::Eager,
14921493
lifecycle_scripts_config: LifecycleScriptsConfig::default(),

cli/lsp/resolver.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -966,6 +966,7 @@ impl<'a> ResolverFactory<'a> {
966966
sys.clone(),
967967
tarball_cache.clone(),
968968
deno_npm_installer::NpmInstallerOptions {
969+
clean_on_install: false,
969970
maybe_lockfile,
970971
maybe_node_modules_path: maybe_node_modules_path.clone(),
971972
lifecycle_scripts: Arc::new(LifecycleScriptsConfig::default()),

cli/npm.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ use deno_runtime::deno_io::FromRawIoHandle;
3434
use deno_semver::package::PackageNv;
3535
use deno_semver::package::PackageReq;
3636
use deno_task_shell::KillSignal;
37+
use sys_traits::PathsInErrorsExt;
3738

3839
use crate::file_fetcher::CliFileFetcher;
3940
use crate::http_util::HttpClientProvider;
@@ -268,9 +269,6 @@ pub enum DenoTaskLifecycleScriptsError {
268269
#[error(transparent)]
269270
Io(#[from] std::io::Error),
270271
#[class(inherit)]
271-
#[error(transparent)]
272-
BinEntries(#[from] deno_npm_installer::BinEntriesError),
273-
#[class(inherit)]
274272
#[error(
275273
"failed to create npm process state tempfile for running lifecycle scripts"
276274
)]
@@ -296,7 +294,7 @@ impl LifecycleScriptsExecutor for DenoTaskLifeCycleScriptsExecutor {
296294
) -> Result<(), AnyError> {
297295
let mut failed_packages = Vec::new();
298296
let sys = CliSys::default();
299-
let mut bin_entries = BinEntries::new(&sys);
297+
let mut bin_entries = BinEntries::new(sys.with_paths_in_errors());
300298
// get custom commands for each bin available in the node_modules dir (essentially
301299
// the scripts that are in `node_modules/.bin`)
302300
let base = self
@@ -567,7 +565,7 @@ impl DenoTaskLifeCycleScriptsExecutor {
567565
snapshot: &NpmResolutionSnapshot,
568566
) -> crate::task_runner::TaskCustomCommands {
569567
let sys = CliSys::default();
570-
let mut bin_entries = BinEntries::new(&sys);
568+
let mut bin_entries = BinEntries::new(sys.with_paths_in_errors());
571569
self
572570
.resolve_custom_commands_from_packages(
573571
extra_info_provider,

cli/tools/clean.rs

Lines changed: 32 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ fn clean_node_modules(
503503
return Ok(());
504504
}
505505

506-
let keep_names = keep_pkgs
506+
let keep_ids = keep_pkgs
507507
.iter()
508508
.map(deno_resolver::npm::get_package_folder_id_folder_name)
509509
.collect::<HashSet<_>>();
@@ -530,8 +530,9 @@ fn clean_node_modules(
530530
};
531531

532532
// TODO(nathanwhit): this probably shouldn't reach directly into this code
533+
let sys = CliSys::default();
533534
let mut setup_cache = deno_npm_installer::LocalSetupCache::load(
534-
CliSys::default(),
535+
sys.clone(),
535536
base.join(".setup-cache.bin"),
536537
);
537538

@@ -542,7 +543,7 @@ fn clean_node_modules(
542543
}
543544
let file_name = entry.file_name();
544545
let file_name = file_name.to_string_lossy();
545-
if keep_names.contains(file_name.as_ref()) || file_name == "node_modules" {
546+
if keep_ids.contains(file_name.as_ref()) || file_name == "node_modules" {
546547
continue;
547548
} else if dry_run {
548549
#[allow(clippy::print_stderr)]
@@ -554,78 +555,51 @@ fn clean_node_modules(
554555
}
555556
}
556557

558+
let mut remove_symlink = |path: &Path| -> std::io::Result<()> {
559+
if dry_run {
560+
#[allow(clippy::print_stderr)]
561+
{
562+
eprintln!(" {}", path.display());
563+
}
564+
Ok(())
565+
} else {
566+
cleaner.remove_file(path, None)
567+
}
568+
};
569+
557570
// remove top level symlinks from node_modules/<package> to node_modules/.deno/<package>
558571
// where the target doesn't exist (because it was removed above)
559-
clean_node_modules_symlinks(
560-
cleaner,
561-
&keep_names,
572+
deno_npm_installer::remove_unused_node_modules_symlinks(
573+
&sys,
562574
dir,
563-
dry_run,
564-
&mut |name| {
575+
&keep_ids,
576+
&mut |name, path| {
565577
setup_cache.remove_root_symlink(name);
578+
remove_symlink(path)
566579
},
567-
)?;
580+
)
581+
.map_err(AnyError::from)?;
568582

569583
// remove symlinks from node_modules/.deno/node_modules/<package> to node_modules/.deno/<package>
570584
// where the target doesn't exist (because it was removed above)
571-
clean_node_modules_symlinks(
572-
cleaner,
573-
&keep_names,
574-
&base.join("node_modules"),
575-
dry_run,
576-
&mut |name| {
585+
let deno_nm = base.join("node_modules");
586+
deno_npm_installer::remove_unused_node_modules_symlinks(
587+
&sys,
588+
&deno_nm,
589+
&keep_ids,
590+
&mut |name, path| {
577591
setup_cache.remove_deno_symlink(name);
592+
remove_symlink(path)
578593
},
579-
)?;
594+
)
595+
.map_err(AnyError::from)?;
580596
if !dry_run {
581597
setup_cache.save();
582598
}
583599

584600
Ok(())
585601
}
586602

587-
// node_modules/.deno/chalk@5.0.1/node_modules/chalk -> chalk@5.0.1
588-
fn node_modules_package_actual_dir_to_name(
589-
path: &Path,
590-
) -> Option<Cow<'_, str>> {
591-
path
592-
.parent()?
593-
.parent()?
594-
.file_name()
595-
.map(|name| name.to_string_lossy())
596-
}
597-
598-
fn clean_node_modules_symlinks(
599-
cleaner: &mut FsCleaner,
600-
keep_names: &HashSet<String>,
601-
dir: &Path,
602-
dry_run: bool,
603-
on_remove: &mut dyn FnMut(&str),
604-
) -> Result<(), AnyError> {
605-
for entry in std::fs::read_dir(dir)? {
606-
let entry = entry?;
607-
let ty = entry.file_type()?;
608-
if ty.is_symlink() {
609-
let target = std::fs::read_link(entry.path())?;
610-
let name = node_modules_package_actual_dir_to_name(&target);
611-
if let Some(name) = name
612-
&& !keep_names.contains(&*name)
613-
{
614-
if dry_run {
615-
#[allow(clippy::print_stderr)]
616-
{
617-
eprintln!(" {}", entry.path().display());
618-
}
619-
} else {
620-
on_remove(&name);
621-
cleaner.remove_file(&entry.path(), None)?;
622-
}
623-
}
624-
}
625-
}
626-
Ok(())
627-
}
628-
629603
#[cfg(test)]
630604
mod tests {
631605
use std::path::Path;

cli/util/fs.rs

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ use deno_config::glob::PathOrPattern;
1111
use deno_config::glob::PathOrPatternSet;
1212
use deno_config::glob::WalkEntry;
1313
use deno_core::ModuleSpecifier;
14-
use deno_core::anyhow::Context;
1514
use deno_core::anyhow::anyhow;
1615
use deno_core::error::AnyError;
16+
use sys_traits::FsMetadataValue;
17+
use sys_traits::PathsInErrorsExt;
1718

1819
use super::progress_bar::UpdateGuard;
1920
use crate::sys::CliSys;
@@ -195,14 +196,16 @@ impl FsCleaner {
195196
}
196197
}
197198

198-
pub fn rm_rf(&mut self, path: &Path) -> Result<(), AnyError> {
199+
pub fn rm_rf(&mut self, path: &Path) -> Result<(), std::io::Error> {
200+
let sys = CliSys::default();
201+
let sys = sys.with_paths_in_errors();
199202
for entry in walkdir::WalkDir::new(path).contents_first(true) {
200-
let entry = entry?;
203+
let entry = entry.map_err(std::io::Error::other)?;
201204

202205
if entry.file_type().is_dir() {
203206
self.dirs_removed += 1;
204207
self.update_progress();
205-
std::fs::remove_dir_all(entry.path())?;
208+
sys.fs_remove_dir_all(entry.path())?;
206209
} else {
207210
self.remove_file(entry.path(), entry.metadata().ok())?;
208211
}
@@ -215,29 +218,13 @@ impl FsCleaner {
215218
&mut self,
216219
path: &Path,
217220
meta: Option<std::fs::Metadata>,
218-
) -> Result<(), AnyError> {
221+
) -> Result<(), std::io::Error> {
219222
if let Some(meta) = meta {
220223
self.bytes_removed += meta.len();
221224
}
222225
self.files_removed += 1;
223226
self.update_progress();
224-
match std::fs::remove_file(path)
225-
.with_context(|| format!("Failed to remove file: {}", path.display()))
226-
{
227-
Err(e) => {
228-
if cfg!(windows)
229-
&& let Ok(meta) = path.symlink_metadata()
230-
&& meta.is_symlink()
231-
{
232-
std::fs::remove_dir(path).with_context(|| {
233-
format!("Failed to remove symlink: {}", path.display())
234-
})?;
235-
return Ok(());
236-
}
237-
Err(e)
238-
}
239-
_ => Ok(()),
240-
}
227+
remove_file_or_symlink(&CliSys::default(), path)
241228
}
242229

243230
fn update_progress(&self) {
@@ -247,6 +234,30 @@ impl FsCleaner {
247234
}
248235
}
249236

237+
pub fn remove_file_or_symlink<
238+
TSys: sys_traits::BaseFsRemoveFile
239+
+ sys_traits::BaseFsRemoveDir
240+
+ sys_traits::BaseFsMetadata,
241+
>(
242+
sys: &TSys,
243+
path: &Path,
244+
) -> Result<(), Error> {
245+
let sys = sys.with_paths_in_errors();
246+
match sys.fs_remove_file(path) {
247+
Err(e) => {
248+
if sys_traits::impls::is_windows()
249+
&& let Ok(meta) = sys.fs_symlink_metadata(path)
250+
&& meta.file_type().is_symlink()
251+
{
252+
sys.fs_remove_dir(path)?;
253+
return Ok(());
254+
}
255+
Err(e)
256+
}
257+
_ => Ok(()),
258+
}
259+
}
260+
250261
#[cfg(test)]
251262
mod tests {
252263
use pretty_assertions::assert_eq;

0 commit comments

Comments
 (0)