diff --git a/.changeset/production-flag-alias.md b/.changeset/production-flag-alias.md new file mode 100644 index 0000000000..96da9f7dad --- /dev/null +++ b/.changeset/production-flag-alias.md @@ -0,0 +1,7 @@ +--- +"@pnpm/exec.commands": patch +"pacquet": patch +"pnpm": patch +--- + +`--production` is accepted again as an alias of `--prod` on `install`, `fetch`, `prune`, `update`, `list`, `why`, and `sbom`, and the install that `verifyDepsBeforeRun` reproduces is now spelled with `--prod`. `pnpm run` no longer aborts with "unexpected argument '--production' found" after a production-only install [#14147](https://github.com/pnpm/pnpm/issues/14147). diff --git a/pnpm/crates/cli/src/cli_args/fetch.rs b/pnpm/crates/cli/src/cli_args/fetch.rs index bf162e2cb8..8d27a5a2c7 100644 --- a/pnpm/crates/cli/src/cli_args/fetch.rs +++ b/pnpm/crates/cli/src/cli_args/fetch.rs @@ -7,7 +7,7 @@ use pnpm_reporter::Reporter; #[derive(Debug, Args)] pub struct FetchArgs { - #[clap(short = 'P', long)] + #[clap(short = 'P', long, visible_alias = "production")] prod: bool, #[clap(short = 'D', long)] dev: bool, diff --git a/pnpm/crates/cli/src/cli_args/install.rs b/pnpm/crates/cli/src/cli_args/install.rs index a28940f71b..9b7623a5b2 100644 --- a/pnpm/crates/cli/src/cli_args/install.rs +++ b/pnpm/crates/cli/src/cli_args/install.rs @@ -60,7 +60,7 @@ impl NodeLinkerArg { pub struct InstallDependencyOptions { /// Install only production dependencies. devDependencies are skipped, /// and removed if already installed. Takes precedence over `NODE_ENV`. - #[arg(short = 'P', long)] + #[arg(short = 'P', long, visible_alias = "production")] prod: bool, /// Install only devDependencies. Regular dependencies are skipped, and /// removed if already installed, regardless of `NODE_ENV`. diff --git a/pnpm/crates/cli/src/cli_args/list.rs b/pnpm/crates/cli/src/cli_args/list.rs index 96b33d9179..0d5f333228 100644 --- a/pnpm/crates/cli/src/cli_args/list.rs +++ b/pnpm/crates/cli/src/cli_args/list.rs @@ -84,7 +84,7 @@ pub struct ListArgs { /// Display only the dependency graph for packages in `dependencies` /// and `optionalDependencies`. - #[clap(short = 'P', long = "prod")] + #[clap(short = 'P', long = "prod", visible_alias = "production")] pub production: bool, /// Display only the dependency graph for packages in `devDependencies`. diff --git a/pnpm/crates/cli/src/cli_args/prune.rs b/pnpm/crates/cli/src/cli_args/prune.rs index 83e2030d4a..1f3e4827dc 100644 --- a/pnpm/crates/cli/src/cli_args/prune.rs +++ b/pnpm/crates/cli/src/cli_args/prune.rs @@ -7,7 +7,7 @@ use pnpm_reporter::Reporter; #[derive(Debug, Args)] pub struct PruneArgs { - #[clap(short = 'P', long)] + #[clap(short = 'P', long, visible_alias = "production")] prod: bool, #[clap(short = 'D', long)] dev: bool, diff --git a/pnpm/crates/cli/src/cli_args/sbom.rs b/pnpm/crates/cli/src/cli_args/sbom.rs index 57d2736e8a..29aef06590 100644 --- a/pnpm/crates/cli/src/cli_args/sbom.rs +++ b/pnpm/crates/cli/src/cli_args/sbom.rs @@ -75,7 +75,7 @@ pub struct SbomArgs { pub supplier: Option, /// Only include production dependencies. - #[clap(long, short = 'P')] + #[clap(long, short = 'P', visible_alias = "production")] pub prod: bool, /// Only include dev dependencies. diff --git a/pnpm/crates/cli/src/cli_args/tests.rs b/pnpm/crates/cli/src/cli_args/tests.rs index 97d10d2fd4..c1945a83f4 100644 --- a/pnpm/crates/cli/src/cli_args/tests.rs +++ b/pnpm/crates/cli/src/cli_args/tests.rs @@ -1100,6 +1100,38 @@ fn store_status_and_add_are_subcommands_of_store() { assert_eq!(add.packages, ["express@4", "typescript@2.1.0"]); } +/// `--production` is the setting name behind `--prod`, and pnpm accepts +/// it wherever `--prod` selects dependency groups — in a command line +/// typed by hand as much as in the install the verify-deps-before-run +/// gate reproduces +/// ([pnpm/pnpm#14147](https://github.com/pnpm/pnpm/issues/14147)). +#[test] +fn production_is_an_alias_of_prod() { + for argv in [ + ["pacquet", "install", "--production"].as_slice(), + ["pacquet", "fetch", "--production"].as_slice(), + ["pacquet", "prune", "--production"].as_slice(), + ["pacquet", "update", "--production"].as_slice(), + ["pacquet", "sbom", "--sbom-format", "spdx", "--production"].as_slice(), + ["pacquet", "list", "--production"].as_slice(), + ["pacquet", "why", "--production", "foo"].as_slice(), + ["pacquet", "audit", "--production"].as_slice(), + ["pacquet", "licenses", "list", "--production"].as_slice(), + ["pacquet", "outdated", "--production"].as_slice(), + ] { + CliArgs::try_parse_from(argv) + .unwrap_or_else(|error| panic!("`{}` must parse: {error}", argv.join(" "))); + } + + let groups = |argv: &[&str]| { + install_args(argv).dependency_options.dependency_groups(true).collect::>() + }; + assert_eq!( + groups(&["pacquet", "install", "--production"]), + groups(&["pacquet", "install", "--prod"]), + ); +} + fn command(argv: &[&str]) -> CliCommand { CliArgs::try_parse_from(argv).expect("parses").command } diff --git a/pnpm/crates/cli/src/cli_args/update.rs b/pnpm/crates/cli/src/cli_args/update.rs index 89175d3240..c9c76ae1ef 100644 --- a/pnpm/crates/cli/src/cli_args/update.rs +++ b/pnpm/crates/cli/src/cli_args/update.rs @@ -24,7 +24,7 @@ use std::{collections::HashSet, path::Path}; #[derive(Debug, Clone, Args)] pub struct UpdateDependencyOptions { /// Update packages only in "dependencies" and "optionalDependencies". - #[clap(short = 'P', long)] + #[clap(short = 'P', long, visible_alias = "production")] prod: bool, /// Update packages only in "devDependencies". #[clap(short = 'D', long)] diff --git a/pnpm/crates/cli/src/cli_args/why.rs b/pnpm/crates/cli/src/cli_args/why.rs index 9d945febde..1d86649d79 100644 --- a/pnpm/crates/cli/src/cli_args/why.rs +++ b/pnpm/crates/cli/src/cli_args/why.rs @@ -47,7 +47,7 @@ pub struct WhyArgs { /// Display only the dependency graph for packages in `dependencies` /// and `optionalDependencies`. - #[clap(short = 'P', long = "prod")] + #[clap(short = 'P', long = "prod", visible_alias = "production")] pub production: bool, /// Display only the dependency graph for packages in `devDependencies`. diff --git a/pnpm/crates/cli/tests/suite/verify_deps_before_run.rs b/pnpm/crates/cli/tests/suite/verify_deps_before_run.rs index d25d3cce33..c8571e2ad2 100644 --- a/pnpm/crates/cli/tests/suite/verify_deps_before_run.rs +++ b/pnpm/crates/cli/tests/suite/verify_deps_before_run.rs @@ -16,15 +16,31 @@ use serde_json::json; use std::{fs, path::Path}; fn write_manifest(workspace: &Path, marker: &Path) { - let manifest = json!({ + write_manifest_with_dependency_groups(workspace, marker, json!({})); +} + +/// The fixture manifest — a `hello` script that touches `marker` — +/// extended with the dependency groups the caller needs. +fn write_manifest_with_dependency_groups( + workspace: &Path, + marker: &Path, + groups: serde_json::Value, +) { + let serde_json::Value::Object(mut manifest) = json!({ "name": "verify-deps-project", "version": "0.0.0", "scripts": { "hello": format!(r#"touch "{}""#, marker.display()), }, - }) - .to_string(); - fs::write(workspace.join("package.json"), manifest).expect("write package.json"); + }) else { + unreachable!("the manifest literal is an object") + }; + let serde_json::Value::Object(groups) = groups else { + panic!("the dependency groups must be an object") + }; + manifest.extend(groups); + fs::write(workspace.join("package.json"), serde_json::Value::Object(manifest).to_string()) + .expect("write package.json"); } /// The default action is `install` (pnpm's @@ -44,6 +60,61 @@ fn default_install_action_installs_before_running_the_script() { drop(root); } +/// The spawned install reproduces the dependency groups the last +/// install recorded, spelled the way the CLI accepts them, so a +/// production-only install leaves `pnpm run` working +/// ([pnpm/pnpm#14147](https://github.com/pnpm/pnpm/issues/14147)). +#[cfg(unix)] +#[test] +fn install_action_reruns_a_production_only_install() { + let CommandTempCwd { pacquet, root, workspace, npmrc_info, .. } = + CommandTempCwd::init().add_mocked_registry(); + let AddMockedRegistry { mock_instance, .. } = npmrc_info; + let marker = workspace.join("marker.txt"); + let write_project = |foo_version: &str| { + write_manifest_with_dependency_groups( + &workspace, + &marker, + json!({ + "dependencies": { + "@pnpm.e2e/foo": foo_version, + }, + "devDependencies": { + "@pnpm.e2e/bar": "100.0.0", + }, + }), + ); + }; + + write_project("100.0.0"); + pacquet.with_args(["install", "--prod"]).assert().success(); + assert!( + !workspace.join("node_modules/@pnpm.e2e/bar").exists(), + "a production-only install must skip devDependencies", + ); + + write_project("100.1.0"); + bump_mtime(&workspace.join("package.json")); + + pacquet_in(&workspace).with_args(["run", "hello"]).assert().success(); + assert!(marker.exists(), "the script must run after the spawned install"); + let installed: serde_json::Value = serde_json::from_str( + &fs::read_to_string(workspace.join("node_modules/@pnpm.e2e/foo/package.json")) + .expect("read the installed @pnpm.e2e/foo manifest"), + ) + .expect("parse the installed @pnpm.e2e/foo manifest"); + assert_eq!( + installed["version"], "100.1.0", + "the spawned install must install the updated production dependency", + ); + assert!( + !workspace.join("node_modules/@pnpm.e2e/bar").exists(), + "the spawned install must keep the recorded production-only groups", + ); + + drop((root, mock_instance)); +} + #[test] fn dedupe_peers_lockfile_regeneration_installs_before_running_the_script() { let CommandTempCwd { pacquet, root, workspace, npmrc_info, .. } = diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/deps_status.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/deps_status.rs index 9ee0b1a640..c6118059fe 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/deps_status.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/deps_status.rs @@ -24,7 +24,7 @@ pub enum RunDepsStatus { /// `warn` and `error` actions. issue: String, /// `pnpm install` arguments reproducing the dependency groups - /// the workspace state recorded (`--production` / `--dev` / + /// the workspace state recorded (`--prod` / `--dev` / /// `--no-optional`), for the `install` and `prompt` actions. install_args: Vec, }, @@ -226,7 +226,7 @@ pub(crate) fn install_args_from_state(state: &WorkspaceState) -> Vec { let dev = settings.dev.unwrap_or(false); let production = settings.production.unwrap_or(false); if production && !dev { - args.push("--production".to_string()); + args.push("--prod".to_string()); } else if dev && !production { args.push("--dev".to_string()); } diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/settings.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/settings.rs index fed071ebc1..bd9a7a8f7f 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/settings.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/settings.rs @@ -71,7 +71,7 @@ pub(crate) fn settings_match( /// they all match. `ignored_workspace_state_settings` lets callers skip /// keys such as `dev` / `optional` / `production`: `pnpm run` / `pnpm /// exec` always execute with the default dependency groups, so those -/// never match the state written by a `--production` / `--no-optional` +/// never match the state written by a `--prod` / `--no-optional` /// install (pnpm's `ignoredWorkspaceStateSettings`). pub(crate) fn first_setting_drift( state: &WorkspaceState, diff --git a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests.rs b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests.rs index c51b716c99..b2ef9ec60c 100644 --- a/pnpm/crates/package-manager/src/optimistic_repeat_install/tests.rs +++ b/pnpm/crates/package-manager/src/optimistic_repeat_install/tests.rs @@ -3,7 +3,7 @@ use super::{ check_optimistic_repeat_install_ignoring, conflict_markers::MAX_LOCKFILE_CONFLICT_SCAN_BYTES, current_pnpmfiles, - deps_status::{RunDepsStatus, check_deps_status_before_run}, + deps_status::{RunDepsStatus, check_deps_status_before_run, install_args_from_state}, manifest_agreement::{LinkedPackagesContext, linked_packages_are_up_to_date}, settings::{current_settings, current_settings_with_catalogs}, timestamps::{FileMtime, lockfile_modified_since, modified_at_or_after}, @@ -2937,3 +2937,25 @@ fn lockfile_check_does_not_self_flag_its_own_baseline() { // A whole second entirely before the baseline is not flagged. assert!(!lockfile_modified_since(coarse, ms + 1_000)); } + +/// The reproduction command spells the dependency-group flags the way +/// the CLI accepts them +/// ([pnpm/pnpm#14147](https://github.com/pnpm/pnpm/issues/14147)). The +/// table mirrors pnpm's `createInstallArgs` test. +#[test] +fn install_args_reproduce_the_recorded_dependency_groups() { + let args = |dev: Option, optional: Option, production: Option| { + let state = WorkspaceState { + settings: WorkspaceStateSettings { dev, optional, production, ..Default::default() }, + ..Default::default() + }; + install_args_from_state(&state) + }; + + assert_eq!(args(None, Some(true), Some(true)), ["--prod"]); + assert_eq!(args(None, Some(false), Some(true)), ["--prod", "--no-optional"]); + assert_eq!(args(Some(true), Some(true), None), ["--dev"]); + assert_eq!(args(Some(true), Some(false), None), ["--dev", "--no-optional"]); + assert_eq!(args(Some(true), Some(true), Some(true)), [] as [&str; 0]); + assert_eq!(args(Some(true), Some(false), Some(true)), ["--no-optional"]); +} diff --git a/pnpm11/exec/commands/src/runDepsStatusCheck.ts b/pnpm11/exec/commands/src/runDepsStatusCheck.ts index 37de0897cd..9169aca172 100644 --- a/pnpm11/exec/commands/src/runDepsStatusCheck.ts +++ b/pnpm11/exec/commands/src/runDepsStatusCheck.ts @@ -13,7 +13,7 @@ export interface RunDepsStatusCheckOptions extends CheckDepsStatusOptions { export async function runDepsStatusCheck (opts: RunDepsStatusCheckOptions): Promise { // the following flags are always the default values during `pnpm run` and `pnpm exec`, - // so they may not match the workspace state after `pnpm install --production|--no-optional` + // so they may not match the workspace state after `pnpm install --prod|--no-optional` const ignoredWorkspaceStateSettings = ['dev', 'optional', 'production'] satisfies Array opts.ignoredWorkspaceStateSettings = ignoredWorkspaceStateSettings @@ -70,7 +70,7 @@ export function createInstallArgs (opts: Pick { test.each([ - [{ production: true, optional: true }, ['--production']], - [{ production: true, optional: false }, ['--production', '--no-optional']], + [{ production: true, optional: true }, ['--prod']], + [{ production: true, optional: false }, ['--prod', '--no-optional']], [{ dev: true, optional: true }, ['--dev']], [{ dev: true, optional: false }, ['--dev', '--no-optional']], [{ production: true, dev: true, optional: true }, []],