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:
1 parent
b4d19c87a1
commit
370dfd4bd0
15 files changed
+151
-19
No files matched your search
@@ -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).
|
||||
@@ -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,
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
@@ -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 }, []],
|
||||
|
||||
Reference in new issue
Block a user