diff --git a/cli/args/flags.rs b/cli/args/flags.rs index aa329e4857cfef..485da07813afd3 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -345,12 +345,16 @@ pub enum InstallFlagsLocal { #[derive(Clone, Debug, Eq, PartialEq)] pub struct InstallTopLevelFlags { pub lockfile_only: bool, + pub production: bool, + pub skip_types: bool, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct InstallEntrypointsFlags { pub entrypoints: Vec, pub lockfile_only: bool, + pub production: bool, + pub skip_types: bool, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -3586,6 +3590,23 @@ These must be added to the path manually if required."), UnstableArgsConfig::Res .conflicts_with("global"), ) .arg(lockfile_only_arg().conflicts_with("global")) + .arg( + Arg::new("prod") + .long("prod") + .alias("production") + .help("Only install production dependencies (excludes devDependencies)") + .action(ArgAction::SetTrue) + .conflicts_with("global") + .conflicts_with("dev"), + ) + .arg( + Arg::new("skip-types") + .long("skip-types") + .help(cstr!("Exclude @types/* packages from installation. +Be careful, as it uses a name-based heuristic and may skip packages that ship runtime code.")) + .action(ArgAction::SetTrue) + .requires("prod"), + ) }) } @@ -6753,12 +6774,16 @@ fn install_parse( return Ok(()); } let lockfile_only = matches.get_flag("lockfile-only"); + let production = matches.get_flag("prod"); + let skip_types = matches.get_flag("skip-types"); if matches.get_flag("entrypoint") { let entrypoints = matches.remove_many::("cmd").unwrap_or_default(); flags.subcommand = DenoSubcommand::Install(InstallFlags::Local( InstallFlagsLocal::Entrypoints(InstallEntrypointsFlags { entrypoints: entrypoints.collect(), lockfile_only, + production, + skip_types, }), )); } else if let Some(add_files) = matches @@ -6777,7 +6802,11 @@ fn install_parse( )) } else { flags.subcommand = DenoSubcommand::Install(InstallFlags::Local( - InstallFlagsLocal::TopLevel(InstallTopLevelFlags { lockfile_only }), + InstallFlagsLocal::TopLevel(InstallTopLevelFlags { + lockfile_only, + production, + skip_types, + }), )); } Ok(()) @@ -14291,6 +14320,92 @@ mod tests { ); } + #[test] + fn install_production() { + let r = flags_from_vec(svec!["deno", "install", "--prod"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Install(InstallFlags::Local( + InstallFlagsLocal::TopLevel(InstallTopLevelFlags { + lockfile_only: false, + production: true, + skip_types: false, + }) + )), + ..Flags::default() + } + ); + } + + #[test] + fn install_production_with_entrypoint() { + let r = flags_from_vec(svec![ + "deno", + "install", + "--prod", + "--entrypoint", + "main.ts" + ]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Install(InstallFlags::Local( + InstallFlagsLocal::Entrypoints(InstallEntrypointsFlags { + entrypoints: svec!["main.ts"], + lockfile_only: false, + production: true, + skip_types: false, + }) + )), + ..Flags::default() + } + ); + } + + #[test] + fn install_production_with_skip_types() { + let r = flags_from_vec(svec!["deno", "install", "--prod", "--skip-types"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Install(InstallFlags::Local( + InstallFlagsLocal::TopLevel(InstallTopLevelFlags { + lockfile_only: false, + production: true, + skip_types: true, + }) + )), + ..Flags::default() + } + ); + } + + #[test] + fn install_skip_types_requires_prod() { + let r = flags_from_vec(svec!["deno", "install", "--skip-types"]); + assert!(r.is_err()); + } + + #[test] + fn install_production_conflicts_with_global() { + let r = flags_from_vec(svec![ + "deno", + "install", + "--prod", + "--global", + "jsr:@std/http/file-server" + ]); + assert!(r.is_err()); + } + + #[test] + fn install_production_conflicts_with_dev() { + let r = + flags_from_vec(svec!["deno", "install", "--prod", "--dev", "npm:chalk"]); + assert!(r.is_err()); + } + #[test] fn jupyter_unstable_flags() { let r = flags_from_vec(svec![ diff --git a/cli/args/mod.rs b/cli/args/mod.rs index 75f6537d21a282..648dd1c2535453 100644 --- a/cli/args/mod.rs +++ b/cli/args/mod.rs @@ -537,6 +537,9 @@ impl CliOptions { DenoSubcommand::Add(_) => GraphKind::All, DenoSubcommand::Cache(_) => GraphKind::All, DenoSubcommand::Check(_) => GraphKind::TypesOnly, + DenoSubcommand::Install(InstallFlags::Local( + InstallFlagsLocal::Entrypoints(flags), + )) if flags.production => GraphKind::CodeOnly, DenoSubcommand::Install(InstallFlags::Local(_)) => GraphKind::All, _ => self.type_check_mode().as_graph_kind(), } diff --git a/cli/factory.rs b/cli/factory.rs index 1fcdf6d7d1a4d0..3ce48d497e6e0d 100644 --- a/cli/factory.rs +++ b/cli/factory.rs @@ -653,6 +653,26 @@ impl CliFactory { ), caching_strategy: cli_options.default_npm_caching_strategy(), lifecycle_scripts_config: cli_options.lifecycle_scripts_config(), + production: match cli_options.sub_command() { + DenoSubcommand::Install(InstallFlags::Local(flags)) => { + match flags { + InstallFlagsLocal::TopLevel(f) => f.production, + InstallFlagsLocal::Entrypoints(f) => f.production, + InstallFlagsLocal::Add(_) => false, + } + } + _ => false, + }, + skip_types: match cli_options.sub_command() { + DenoSubcommand::Install(InstallFlags::Local(flags)) => { + match flags { + InstallFlagsLocal::TopLevel(f) => f.skip_types, + InstallFlagsLocal::Entrypoints(f) => f.skip_types, + InstallFlagsLocal::Add(_) => false, + } + } + _ => false, + }, resolve_npm_resolution_snapshot: Box::new(|| { deno_lib::args::resolve_npm_resolution_snapshot(&CliSys::default()) }), diff --git a/cli/lib.rs b/cli/lib.rs index 9059464b5297ec..8fb4aa1b6ac9b2 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -168,6 +168,8 @@ async fn run_subcommand( self::args::InstallEntrypointsFlags { entrypoints: cache_flags.files, lockfile_only: false, + production: false, + skip_types: false, }, ) .await diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index 694e377ab70ce3..c4faa1f47ff904 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -1497,6 +1497,8 @@ impl ConfigData { cache_setting: NpmCacheSetting::Use, caching_strategy: NpmCachingStrategy::Eager, lifecycle_scripts_config: LifecycleScriptsConfig::default(), + production: false, + skip_types: false, resolve_npm_resolution_snapshot: Box::new(|| Ok(None)), }, ); diff --git a/cli/tools/installer/global.rs b/cli/tools/installer/global.rs index 6a52949b71b505..244b82d864a53d 100644 --- a/cli/tools/installer/global.rs +++ b/cli/tools/installer/global.rs @@ -517,6 +517,8 @@ async fn setup_config_dir( let entrypoint_flags = InstallEntrypointsFlags { lockfile_only: false, entrypoints: vec![bin_name_and_url.module_url.to_string()], + production: false, + skip_types: false, }; new_flags.subcommand = DenoSubcommand::Install(InstallFlags::Local( InstallFlagsLocal::Entrypoints(entrypoint_flags.clone()), diff --git a/libs/npm_installer/factory.rs b/libs/npm_installer/factory.rs index 482e0755c03e31..c5afb3e8ee6247 100644 --- a/libs/npm_installer/factory.rs +++ b/libs/npm_installer/factory.rs @@ -54,6 +54,10 @@ pub struct NpmInstallerFactoryOptions { pub caching_strategy: NpmCachingStrategy, pub clean_on_install: bool, pub lifecycle_scripts_config: LifecycleScriptsConfig, + /// Only install production dependencies (excludes devDependencies). + pub production: bool, + /// Exclude @types/* packages from installation. + pub skip_types: bool, /// Resolves the npm resolution snapshot from the environment. pub resolve_npm_resolution_snapshot: ResolveNpmResolutionSnapshotFn, } @@ -362,6 +366,8 @@ impl< npm_cache.clone(), Arc::new(NpmInstallDepsProvider::from_workspace( &workspace_factory.workspace_directory()?.workspace, + self.options.production, + self.options.skip_types, )), registry_info_provider.clone(), self.resolver_factory.npm_resolution().clone(), diff --git a/libs/npm_installer/package_json.rs b/libs/npm_installer/package_json.rs index 631c3ca097092d..12b9e1d377627f 100644 --- a/libs/npm_installer/package_json.rs +++ b/libs/npm_installer/package_json.rs @@ -59,7 +59,11 @@ impl NpmInstallDepsProvider { Self::default() } - pub fn from_workspace(workspace: &Arc) -> Self { + pub fn from_workspace( + workspace: &Arc, + production: bool, + skip_types: bool, + ) -> Self { // todo(dsherret): estimate capacity? let mut local_pkgs = Vec::new(); let mut remote_pkgs = Vec::new(); @@ -83,6 +87,11 @@ impl NpmInstallDepsProvider { continue; }; let pkg_req = npm_req_ref.into_inner().req; + + if skip_types && pkg_req.name.starts_with("@types/") { + continue; + } + let workspace_pkg = workspace_npm_pkgs .iter() .find(|pkg| pkg.matches_req(&pkg_req)); @@ -112,9 +121,13 @@ impl NpmInstallDepsProvider { let mut pkg_pkgs = Vec::with_capacity( deps.dependencies.len() + deps.dev_dependencies.len(), ); - for (alias, dep) in - deps.dependencies.iter().chain(deps.dev_dependencies.iter()) - { + let empty = Default::default(); + let dev_deps = if production { + &empty + } else { + &deps.dev_dependencies + }; + for (alias, dep) in deps.dependencies.iter().chain(dev_deps.iter()) { let dep = match dep { Ok(dep) => dep, Err(err) => { @@ -136,6 +149,9 @@ impl NpmInstallDepsProvider { }) } PackageJsonDepValue::Req(pkg_req) => { + if skip_types && pkg_req.name.starts_with("@types/") { + continue; + } let workspace_pkg = workspace_npm_pkgs.iter().find(|pkg| { pkg.matches_req(pkg_req) // do not resolve to the current package diff --git a/tests/specs/install/install_production/__test__.jsonc b/tests/specs/install/install_production/__test__.jsonc new file mode 100644 index 00000000000000..325a98d99d6db3 --- /dev/null +++ b/tests/specs/install/install_production/__test__.jsonc @@ -0,0 +1,123 @@ +{ + "tests": { + "prod_skips_dev_dependencies": { + "tempDir": true, + "steps": [ + { + "args": "install --prod", + "output": "install.out" + }, + { + // Verify the production dependency is installed + "args": "run --cached-only main.js", + "output": "main.out" + }, + { + // Verify the devDependency is NOT in node_modules + "args": [ + "eval", + "try { Deno.statSync('node_modules/@denotest/bin'); console.log('devDep exists'); } catch { console.log('devDep not found'); }" + ], + "output": "no_dev_dep.out" + }, + { + // Verify @types/* package IS still in node_modules (--prod alone doesn't strip types) + "args": [ + "eval", + "try { Deno.statSync('node_modules/@types/denotest__index-export-no-types'); console.log('@types exists'); } catch { console.log('@types not found'); }" + ], + "output": "has_types.out" + } + ] + }, + "prod_skip_types_excludes_at_types": { + "tempDir": true, + "steps": [ + { + "args": "install --prod --skip-types", + "output": "install.out" + }, + { + // Verify the production dependency is installed + "args": "run --cached-only main.js", + "output": "main.out" + }, + { + // Verify @types/* package is NOT in node_modules + "args": [ + "eval", + "try { Deno.statSync('node_modules/@types/denotest__index-export-no-types'); console.log('@types exists'); } catch { console.log('@types not found'); }" + ], + "output": "no_types.out" + } + ] + }, + "without_prod_installs_all": { + "tempDir": true, + "steps": [ + { + "args": "install", + "output": "[WILDCARD]" + }, + { + // Verify the devDependency IS in node_modules + "args": [ + "eval", + "try { Deno.statSync('node_modules/@denotest/bin'); console.log('devDep exists'); } catch { console.log('devDep not found'); }" + ], + "output": "has_dev_dep.out" + }, + { + // Verify @types/* package IS in node_modules + "args": [ + "eval", + "try { Deno.statSync('node_modules/@types/denotest__index-export-no-types'); console.log('@types exists'); } catch { console.log('@types not found'); }" + ], + "output": "has_types.out" + } + ] + }, + "entrypoint_prod_skips_type_only_imports": { + "tempDir": true, + "steps": [ + { + "args": "install --prod --entrypoint entrypoint.ts", + "output": "install.out" + }, + { + // Verify the code dependency is installed + "args": [ + "eval", + "try { Deno.statSync('node_modules/@denotest/esm-basic'); console.log('codeDep exists'); } catch { console.log('codeDep not found'); }" + ], + "output": "has_code_dep.out" + }, + { + // Verify the type-only dependency is NOT installed + "args": [ + "eval", + "try { Deno.statSync('node_modules/@denotest/types'); console.log('typesDep exists'); } catch { console.log('typesDep not found'); }" + ], + "output": "no_types_dep.out" + } + ] + }, + "entrypoint_without_prod_installs_type_imports": { + "tempDir": true, + "steps": [ + { + "args": "install --entrypoint entrypoint.ts", + "output": "[WILDCARD]" + }, + { + // Verify the type-only dependency IS installed + "args": [ + "eval", + "try { Deno.statSync('node_modules/@denotest/types'); console.log('typesDep exists'); } catch { console.log('typesDep not found'); }" + ], + "output": "has_types_dep.out" + } + ] + } + } +} diff --git a/tests/specs/install/install_production/entrypoint.ts b/tests/specs/install/install_production/entrypoint.ts new file mode 100644 index 00000000000000..505e20c56248d7 --- /dev/null +++ b/tests/specs/install/install_production/entrypoint.ts @@ -0,0 +1,6 @@ +import { getValue, setValue } from "npm:@denotest/esm-basic"; +import type { Fizzbuzz } from "npm:@denotest/types"; + +const _value: Fizzbuzz = { fizz: "fizz", buzz: "buzz" }; +setValue(42); +console.log(getValue()); diff --git a/tests/specs/install/install_production/has_code_dep.out b/tests/specs/install/install_production/has_code_dep.out new file mode 100644 index 00000000000000..8c64f65b1bc7d9 --- /dev/null +++ b/tests/specs/install/install_production/has_code_dep.out @@ -0,0 +1 @@ +codeDep exists diff --git a/tests/specs/install/install_production/has_dev_dep.out b/tests/specs/install/install_production/has_dev_dep.out new file mode 100644 index 00000000000000..a6a0be2d62c247 --- /dev/null +++ b/tests/specs/install/install_production/has_dev_dep.out @@ -0,0 +1 @@ +devDep exists diff --git a/tests/specs/install/install_production/has_types.out b/tests/specs/install/install_production/has_types.out new file mode 100644 index 00000000000000..ed31de4bbce7ca --- /dev/null +++ b/tests/specs/install/install_production/has_types.out @@ -0,0 +1 @@ +@types exists diff --git a/tests/specs/install/install_production/has_types_dep.out b/tests/specs/install/install_production/has_types_dep.out new file mode 100644 index 00000000000000..a297c28c349ca2 --- /dev/null +++ b/tests/specs/install/install_production/has_types_dep.out @@ -0,0 +1 @@ +typesDep exists diff --git a/tests/specs/install/install_production/install.out b/tests/specs/install/install_production/install.out new file mode 100644 index 00000000000000..9ce4b1be79468c --- /dev/null +++ b/tests/specs/install/install_production/install.out @@ -0,0 +1 @@ +[WILDCARD] diff --git a/tests/specs/install/install_production/main.js b/tests/specs/install/install_production/main.js new file mode 100644 index 00000000000000..8c74027723b1c8 --- /dev/null +++ b/tests/specs/install/install_production/main.js @@ -0,0 +1,3 @@ +import { getValue, setValue } from "@denotest/esm-basic"; +setValue(42); +console.log(getValue()); diff --git a/tests/specs/install/install_production/main.out b/tests/specs/install/install_production/main.out new file mode 100644 index 00000000000000..d81cc0710eb6cf --- /dev/null +++ b/tests/specs/install/install_production/main.out @@ -0,0 +1 @@ +42 diff --git a/tests/specs/install/install_production/no_dev_dep.out b/tests/specs/install/install_production/no_dev_dep.out new file mode 100644 index 00000000000000..59d68277d0034c --- /dev/null +++ b/tests/specs/install/install_production/no_dev_dep.out @@ -0,0 +1 @@ +devDep not found diff --git a/tests/specs/install/install_production/no_types.out b/tests/specs/install/install_production/no_types.out new file mode 100644 index 00000000000000..7159de51c6726e --- /dev/null +++ b/tests/specs/install/install_production/no_types.out @@ -0,0 +1 @@ +@types not found diff --git a/tests/specs/install/install_production/no_types_dep.out b/tests/specs/install/install_production/no_types_dep.out new file mode 100644 index 00000000000000..b8036af5267731 --- /dev/null +++ b/tests/specs/install/install_production/no_types_dep.out @@ -0,0 +1 @@ +typesDep not found diff --git a/tests/specs/install/install_production/package.json b/tests/specs/install/install_production/package.json new file mode 100644 index 00000000000000..3b499f30713d84 --- /dev/null +++ b/tests/specs/install/install_production/package.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "@denotest/esm-basic": "*", + "@types/denotest__index-export-no-types": "*" + }, + "devDependencies": { + "@denotest/bin": "*" + } +}