fix(cli): accept --production, and reproduce a prod install as --prod (#14148)

The Rust CLI registers only `--prod` on `install`, so the `pnpm install …`
command the verify-deps-before-run gate reproduces from a production-only
install — built with pnpm's `--production` spelling — was rejected by the
argument parser, aborting every `pnpm run` (Closes pnpm/pnpm#14147).

`--production` is the setting name behind `--prod`, and the TypeScript CLI
accepts it on every command where `--prod` selects dependency groups, so
restore it as an alias there too (`audit`, `licenses` and `outdated` already
had it), and emit the documented `--prod` in the reproduction command in
both stacks.

Closes #14147
This commit is contained in:
Zoltan Kochan authored and GitHub committed 2026-08-25 11:27:03 +02:00
1 parent b4d19c87a1
commit 370dfd4bd0
15 files changed
+151 -19

No files matched your search

+7
View File
@@ -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).
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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`.
+1 -1
View File
@@ -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`.
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -75,7 +75,7 @@ pub struct SbomArgs {
pub supplier: Option<String>,
/// Only include production dependencies.
#[clap(long, short = 'P')]
#[clap(long, short = 'P', visible_alias = "production")]
pub prod: bool,
/// Only include dev dependencies.
+32
View File
@@ -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::<Vec<_>>()
};
assert_eq!(
groups(&["pacquet", "install", "--production"]),
groups(&["pacquet", "install", "--prod"]),
);
}
fn command(argv: &[&str]) -> CliCommand {
CliArgs::try_parse_from(argv).expect("parses").command
}
+1 -1
View File
@@ -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)]
+1 -1
View File
@@ -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`.
@@ -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, .. } =
@@ -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<String>,
},
@@ -226,7 +226,7 @@ pub(crate) fn install_args_from_state(state: &WorkspaceState) -> Vec<String> {
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());
}
@@ -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,
@@ -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<bool>, optional: Option<bool>, production: Option<bool>| {
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"]);
}
@@ -13,7 +13,7 @@ export interface RunDepsStatusCheckOptions extends CheckDepsStatusOptions {
export async function runDepsStatusCheck (opts: RunDepsStatusCheckOptions): Promise<void> {
// 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<keyof WorkspaceStateSettings>
opts.ignoredWorkspaceStateSettings = ignoredWorkspaceStateSettings
@@ -70,7 +70,7 @@ export function createInstallArgs (opts: Pick<WorkspaceStateSettings, 'dev' | 'o
if (!opts) return args
const { dev, optional, production } = opts
if (production && !dev) {
args.push('--production')
args.push('--prod')
} else if (dev && !production) {
args.push('--dev')
}
@@ -4,8 +4,8 @@ import { createInstallArgs } from '../src/runDepsStatusCheck.js'
describe('createInstallArgs', () => {
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 }, []],