mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-28 09:55:39 -04:00
feat(cli): port the root command to pacquet (#12632)
* feat(cli): port the `root` command to pacquet Add `pacquet root`, ported from pnpm11/pnpm/src/cmd/root.ts. Prints `<dir>/node_modules` built from the canonicalized `--dir`. It hardcodes the `node_modules` leaf and deliberately does not read `config.modules_dir`: pnpm's handler ignores a configured modules-dir, and pacquet re-anchors `config.modules_dir` to the workspace root inside a workspace, whereas pnpm's `root` uses `config.dir` (the cwd). Anchoring on `--dir` keeps parity in both cases. `--global` / `-g` is rejected with a "not supported yet" error: pacquet has no global packages directory yet, deferred to a shared follow-up that also unblocks `bin -g`. Related to pnpm/pnpm#11633. * fix(cli): gate `root` differential test on Windows and type its `--global` error The `root_matches_pnpm_from_a_workspace_subdir` differential test spawns the real `pnpm`, which on Windows is a `pnpm.cmd` shim that `std::process::Command` cannot resolve via `PATHEXT` ("program not found"). Gate it with `#[cfg_attr(target_os = "windows", ignore)]`, matching the `cfg(unix)` gating in `pnpm_compatibility` and `hoist`. The other three `root` tests spawn only `pacquet` and keep running on Windows. Replace the bare `miette::miette!` rejection of `pacquet root --global` with a typed `RootError::GlobalUnsupported` diagnostic (code `pacquet_cli::root_global_unsupported`), mirroring `runtime`'s `GlobalUnsupported`, so the refusal carries a stable error code.
This commit is contained in:
committed by
GitHub
parent
47685fddab
commit
32d4d3f1f0
@@ -26,6 +26,7 @@ pub mod rebuild;
|
||||
pub mod recursive;
|
||||
pub mod remove;
|
||||
pub mod restart;
|
||||
pub mod root;
|
||||
pub mod run;
|
||||
pub mod runtime;
|
||||
pub mod sanitize;
|
||||
@@ -73,6 +74,7 @@ use prune::PruneArgs;
|
||||
use rebuild::RebuildArgs;
|
||||
use remove::RemoveArgs;
|
||||
use restart::RestartArgs;
|
||||
use root::RootArgs;
|
||||
use run::RunArgs;
|
||||
use runtime::RuntimeArgs;
|
||||
use serde_json::Value;
|
||||
@@ -227,6 +229,8 @@ pub enum CliCommand {
|
||||
/// Manage runtimes.
|
||||
#[clap(visible_alias = "rt")]
|
||||
Runtime(RuntimeArgs),
|
||||
/// Print the effective `node_modules` directory.
|
||||
Root(RootArgs),
|
||||
/// Managing the package store.
|
||||
#[clap(subcommand)]
|
||||
Store(StoreCommand),
|
||||
@@ -754,6 +758,10 @@ impl CliArgs {
|
||||
ReporterType::Silent => Box::pin(args.run::<SilentReporter>(command_state)),
|
||||
}
|
||||
}
|
||||
CliCommand::Root(args) => {
|
||||
args.run(dir_ref)?;
|
||||
Box::pin(std::future::ready(Ok(())))
|
||||
}
|
||||
CliCommand::Store(command) => {
|
||||
command.run(|| config().map(|m| &*m))?;
|
||||
Box::pin(std::future::ready(Ok(())))
|
||||
|
||||
46
pacquet/crates/cli/src/cli_args/root.rs
Normal file
46
pacquet/crates/cli/src/cli_args/root.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use clap::Args;
|
||||
use derive_more::{Display, Error};
|
||||
use miette::Diagnostic;
|
||||
use std::path::Path;
|
||||
|
||||
/// `pacquet root`: print the effective `node_modules` directory.
|
||||
///
|
||||
/// Ports the non-global path of pnpm's `root` handler, which is
|
||||
/// `path.join(opts.dir, 'node_modules')`
|
||||
/// (<https://github.com/pnpm/pnpm/blob/d3f68e2aa4/pnpm11/pnpm/src/cmd/root.ts>):
|
||||
/// the leaf is the hardcoded string `node_modules` — a configured
|
||||
/// `modules-dir` is ignored — and the anchor is `config.dir`, which pnpm
|
||||
/// sets to the realpath of the CLI directory (the cwd, not the workspace
|
||||
/// root). So this prints `<dir>/node_modules` from the already-canonicalized
|
||||
/// `--dir` and deliberately does NOT read `config.modules_dir`, which pacquet
|
||||
/// re-anchors to the workspace root inside a workspace.
|
||||
#[derive(Debug, Args)]
|
||||
pub struct RootArgs {
|
||||
/// Print the global packages directory
|
||||
#[clap(short = 'g', long)]
|
||||
pub global: bool,
|
||||
}
|
||||
|
||||
/// Errors specific to `pacquet root`.
|
||||
#[derive(Debug, Display, Error, Diagnostic, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum RootError {
|
||||
/// `--global` is rejected because the global-dir machinery (pnpm's
|
||||
/// `@pnpm/global.commands`) is not ported to pacquet yet; refuse rather
|
||||
/// than print a wrong path.
|
||||
#[display(
|
||||
"`pacquet root --global` is not supported yet; global package management has not been ported to pacquet."
|
||||
)]
|
||||
#[diagnostic(code(pacquet_cli::root_global_unsupported))]
|
||||
GlobalUnsupported,
|
||||
}
|
||||
|
||||
impl RootArgs {
|
||||
pub fn run(self, dir: &Path) -> miette::Result<()> {
|
||||
if self.global {
|
||||
return Err(RootError::GlobalUnsupported.into());
|
||||
}
|
||||
println!("{}", dir.join("node_modules").display());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
123
pacquet/crates/cli/tests/root.rs
Normal file
123
pacquet/crates/cli/tests/root.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use assert_cmd::prelude::*;
|
||||
use command_extra::CommandExtra;
|
||||
use pacquet_testing_utils::bin::CommandTempCwd;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
/// Canonicalize a path the way the production CLI does. The CLI runs
|
||||
/// `dunce::canonicalize` on `--dir`, so the printed path is the resolved
|
||||
/// form — e.g. on macOS a `/var/folders/...` temp dir surfaces as
|
||||
/// `/private/var/folders/...`. Mirror that so the expected value matches.
|
||||
fn canonicalize(path: &Path) -> PathBuf {
|
||||
dunce::canonicalize(path).expect("canonicalize path")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_prints_the_local_node_modules_dir() {
|
||||
let CommandTempCwd { pacquet, root, workspace, .. } = CommandTempCwd::init();
|
||||
|
||||
let output = pacquet.with_args(["root"]).output().expect("run pacquet root");
|
||||
dbg!(&output);
|
||||
assert!(output.status.success(), "pacquet root should succeed");
|
||||
|
||||
// Deliberately not trimmed, unlike `store path` — pnpm's `root` handler
|
||||
// emits the path with its trailing newline (`${path}\n`).
|
||||
let expected = format!("{}\n", canonicalize(&workspace).join("node_modules").display());
|
||||
assert_eq!(String::from_utf8_lossy(&output.stdout), expected);
|
||||
|
||||
drop(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_ignores_a_custom_modules_dir() {
|
||||
// pnpm's `root` hardcodes the `node_modules` leaf, so a configured
|
||||
// modules-dir must NOT change its output. pacquet matches by anchoring on
|
||||
// `--dir` and never reading `config.modules_dir` in this command.
|
||||
let CommandTempCwd { pacquet, root, workspace, .. } = CommandTempCwd::init();
|
||||
fs::write(workspace.join("pnpm-workspace.yaml"), "modulesDir: custom_nm\n")
|
||||
.expect("write pnpm-workspace.yaml");
|
||||
|
||||
let output = pacquet.with_args(["root"]).output().expect("run pacquet root");
|
||||
dbg!(&output);
|
||||
assert!(output.status.success(), "pacquet root should succeed");
|
||||
|
||||
let expected = format!("{}\n", canonicalize(&workspace).join("node_modules").display());
|
||||
assert_eq!(String::from_utf8_lossy(&output.stdout), expected);
|
||||
|
||||
drop(root);
|
||||
}
|
||||
|
||||
/// `--global` / `-g` is rejected until global package management is ported.
|
||||
#[test]
|
||||
fn root_global_is_not_supported_yet() {
|
||||
let CommandTempCwd { pacquet, root, .. } = CommandTempCwd::init();
|
||||
|
||||
let output = pacquet.with_args(["root", "-g"]).output().expect("run pacquet root -g");
|
||||
dbg!(&output);
|
||||
assert!(!output.status.success(), "pacquet root -g should fail until global support lands");
|
||||
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(stderr.contains("not supported yet"), "stderr should explain the gap: {stderr}");
|
||||
|
||||
drop(root);
|
||||
}
|
||||
|
||||
/// Differential parity: from a workspace subdirectory pnpm's `root` prints the
|
||||
/// cwd's `node_modules` (its `config.dir` is the cwd, not the workspace root).
|
||||
/// pacquet must print byte-identical output.
|
||||
///
|
||||
/// Skipped on Windows, where pnpm is installed as a `pnpm.cmd` shim and
|
||||
/// `std::process::Command` does not honor `PATHEXT`, so `Command::new("pnpm")`
|
||||
/// fails with "program not found" (the same reason `pnpm_compatibility.rs` and
|
||||
/// `hoist.rs` gate their pnpm-spawning tests). The three tests above spawn only
|
||||
/// `pacquet`, so they keep running on Windows.
|
||||
#[test]
|
||||
#[cfg_attr(
|
||||
target_os = "windows",
|
||||
ignore = "spawns the external `pnpm` shim (`pnpm.cmd`); std::process::Command can't resolve it via PATHEXT"
|
||||
)]
|
||||
fn root_matches_pnpm_from_a_workspace_subdir() {
|
||||
let CommandTempCwd { root, workspace, .. } = CommandTempCwd::init();
|
||||
|
||||
fs::write(workspace.join("pnpm-workspace.yaml"), "packages:\n - \"packages/*\"\n")
|
||||
.expect("write pnpm-workspace.yaml");
|
||||
fs::write(workspace.join("package.json"), r#"{ "name": "wsroot", "version": "1.0.0" }"#)
|
||||
.expect("write workspace-root package.json");
|
||||
let member = workspace.join("packages/foo");
|
||||
fs::create_dir_all(&member).expect("create workspace member dir");
|
||||
fs::write(member.join("package.json"), r#"{ "name": "foo", "version": "1.0.0" }"#)
|
||||
.expect("write member package.json");
|
||||
|
||||
let pacquet_out = Command::cargo_bin("pacquet")
|
||||
.expect("find the pacquet binary")
|
||||
.with_current_dir(&member)
|
||||
.with_args(["root"])
|
||||
.output()
|
||||
.expect("run pacquet root in the subdir");
|
||||
assert!(pacquet_out.status.success(), "pacquet root should succeed in the subdir");
|
||||
|
||||
let pnpm_out = Command::new("pnpm")
|
||||
.with_current_dir(&member)
|
||||
.with_args(["root"])
|
||||
.output()
|
||||
.expect("run pnpm root in the subdir");
|
||||
assert!(
|
||||
pnpm_out.status.success(),
|
||||
"pnpm root failed: {}",
|
||||
String::from_utf8_lossy(&pnpm_out.stderr),
|
||||
);
|
||||
|
||||
let pacquet_stdout = String::from_utf8_lossy(&pacquet_out.stdout);
|
||||
let pnpm_stdout = String::from_utf8_lossy(&pnpm_out.stdout);
|
||||
eprintln!("pacquet: {pacquet_stdout:?}\npnpm: {pnpm_stdout:?}");
|
||||
assert_eq!(
|
||||
pacquet_stdout, pnpm_stdout,
|
||||
"pacquet root must match pnpm root from a workspace subdir (cwd, not workspace root)",
|
||||
);
|
||||
|
||||
drop(root);
|
||||
}
|
||||
Reference in New Issue
Block a user