Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ls): support output format #9031

Merged
merged 7 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion crates/turborepo-lib/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,25 @@ pub enum DaemonCommand {
Logs,
}

#[derive(Copy, Clone, Debug, Default, ValueEnum, Serialize, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum OutputFormat {
/// Output in a human-readable format
#[default]
Pretty,
/// Output in JSON format for direct parsing
Json,
}

impl fmt::Display for OutputFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
OutputFormat::Pretty => "pretty",
OutputFormat::Json => "json",
})
}
}

#[derive(Subcommand, Copy, Clone, Debug, PartialEq)]
pub enum TelemetryCommand {
/// Enables anonymous telemetry
Expand Down Expand Up @@ -507,6 +526,9 @@ pub enum Command {
/// Get insight into a specific package, such as
/// its dependencies and tasks
packages: Vec<String>,
/// Output format
#[clap(long, value_enum)]
output: Option<OutputFormat>,
},
/// Link your local directory to a Vercel organization and enable remote
/// caching.
Expand Down Expand Up @@ -1165,16 +1187,18 @@ pub async fn run(
affected,
filter,
packages,
output,
} => {
warn!("ls command is experimental and may change in the future");
let event = CommandEventBuilder::new("info").with_parent(&root_telemetry);

event.track_call();
let affected = *affected;
let output = *output;
let filter = filter.clone();
let packages = packages.clone();
let base = CommandBase::new(cli_args, repo_root, version, color_config);
ls::run(base, packages, event, filter, affected).await?;
ls::run(base, packages, event, filter, affected, output).await?;

Ok(0)
}
Expand Down
154 changes: 133 additions & 21 deletions crates/turborepo-lib/src/commands/ls.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! A command for outputting info about packages and tasks in a turborepo.

use miette::Diagnostic;
use serde::Serialize;
use thiserror::Error;
use turbopath::AnchoredSystemPath;
use turborepo_repository::{
Expand All @@ -12,7 +13,7 @@ use turborepo_ui::{color, cprint, cprintln, ColorConfig, BOLD, BOLD_GREEN, GREY}

use crate::{
cli,
cli::{Command, ExecutionArgs},
cli::{Command, ExecutionArgs, OutputFormat},
commands::{run::get_signal, CommandBase},
run::{builder::RunBuilder, Run},
signal::SignalHandler,
Expand All @@ -24,25 +25,100 @@ pub enum Error {
PackageNotFound { package: String },
}

#[derive(Serialize)]
struct ItemsWithCount<T> {
count: usize,
items: Vec<T>,
}

#[derive(Clone, Serialize)]
#[serde(into = "RepositoryDetailsDisplay<'a>")]
struct RepositoryDetails<'a> {
color_config: ColorConfig,
package_manager: &'a PackageManager,
packages: Vec<(&'a PackageName, &'a AnchoredSystemPath)>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct RepositoryDetailsDisplay<'a> {
package_manager: &'a PackageManager,
packages: ItemsWithCount<PackageDetailDisplay>,
}

#[derive(Serialize)]
struct PackageDetailDisplay {
name: String,
path: String,
}

impl<'a> From<RepositoryDetails<'a>> for RepositoryDetailsDisplay<'a> {
fn from(val: RepositoryDetails<'a>) -> Self {
RepositoryDetailsDisplay {
package_manager: val.package_manager,
packages: ItemsWithCount {
count: val.packages.len(),
items: val
.packages
.into_iter()
.map(|(name, path)| PackageDetailDisplay {
name: name.to_string(),
path: path.to_string(),
})
.collect(),
},
}
}
}

#[derive(Clone, Serialize)]
struct PackageTask<'a> {
name: &'a str,
command: &'a str,
}

#[derive(Clone, Serialize)]
#[serde(into = "PackageDetailsDisplay<'a>")]
struct PackageDetails<'a> {
#[serde(skip)]
color_config: ColorConfig,
name: &'a str,
tasks: Vec<(&'a str, &'a str)>,
tasks: Vec<PackageTask<'a>>,
dependencies: Vec<&'a str>,
}

#[derive(Clone, Serialize)]
struct PackageDetailsList<'a> {
packages: Vec<PackageDetails<'a>>,
}

#[derive(Serialize)]
struct PackageDetailsDisplay<'a> {
name: &'a str,
tasks: ItemsWithCount<PackageTask<'a>>,
dependencies: Vec<&'a str>,
}

impl<'a> From<PackageDetails<'a>> for PackageDetailsDisplay<'a> {
fn from(val: PackageDetails<'a>) -> Self {
PackageDetailsDisplay {
name: val.name,
dependencies: val.dependencies,
tasks: ItemsWithCount {
count: val.tasks.len(),
items: val.tasks,
},
}
}
}

pub async fn run(
mut base: CommandBase,
packages: Vec<String>,
telemetry: CommandEventBuilder,
filter: Vec<String>,
affected: bool,
output: Option<OutputFormat>,
) -> Result<(), cli::Error> {
let signal = get_signal()?;
let handler = SignalHandler::new(signal);
Expand All @@ -61,11 +137,26 @@ pub async fn run(
let run = run_builder.build(&handler, telemetry).await?;

if packages.is_empty() {
RepositoryDetails::new(&run).print()?;
RepositoryDetails::new(&run).print(output)?;
} else {
for package in packages {
let package_details = PackageDetails::new(&run, &package)?;
package_details.print();
match output {
Some(OutputFormat::Json) => {
let mut package_details_list = PackageDetailsList { packages: vec![] };
// collect all package details
for package in &packages {
let package_details = PackageDetails::new(&run, package)?;
package_details_list.packages.push(package_details);
}

let as_json = serde_json::to_string_pretty(&package_details_list)?;
println!("{}", as_json);
}
Some(OutputFormat::Pretty) | None => {
for package in packages {
let package_details = PackageDetails::new(&run, &package)?;
package_details.print();
}
}
}
}

Expand Down Expand Up @@ -99,21 +190,42 @@ impl<'a> RepositoryDetails<'a> {
packages,
}
}
fn print(&self) -> Result<(), cli::Error> {
if self.packages.len() == 1 {
cprintln!(self.color_config, BOLD, "{} package\n", self.packages.len());
} else {
cprintln!(
self.color_config,
BOLD,
"{} packages\n",
self.packages.len()
);
}
fn pretty_print(&self) {
let package_copy = match self.packages.len() {
0 => "no packages",
1 => "package",
_ => "packages",
};

cprint!(
self.color_config,
BOLD,
"{} {} ",
self.packages.len(),
package_copy
);
cprintln!(self.color_config, GREY, "({})\n", self.package_manager);

for (package_name, entry) in &self.packages {
println!(" {} {}", package_name, GREY.apply_to(entry));
}
}

fn json_print(&self) -> Result<(), cli::Error> {
let as_json = serde_json::to_string_pretty(&self)?;
println!("{}", as_json);
Ok(())
}

fn print(&self, output: Option<OutputFormat>) -> Result<(), cli::Error> {
match output {
Some(OutputFormat::Json) => {
self.json_print()?;
}
Some(OutputFormat::Pretty) | None => {
self.pretty_print();
}
}

Ok(())
}
Expand Down Expand Up @@ -153,7 +265,7 @@ impl<'a> PackageDetails<'a> {
tasks: package_json
.scripts
.iter()
.map(|(name, command)| (name.as_str(), command.as_str()))
.map(|(name, command)| PackageTask { name, command })
.collect(),
})
}
Expand All @@ -180,11 +292,11 @@ impl<'a> PackageDetails<'a> {
} else {
println!();
}
for (name, command) in &self.tasks {
for task in &self.tasks {
println!(
" {}: {}",
name,
color!(self.color_config, GREY, "{}", command)
task.name,
color!(self.color_config, GREY, "{}", task.command)
);
}
println!();
Expand Down
12 changes: 6 additions & 6 deletions turborepo-tests/integration/tests/affected.t
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Validate that we only run `my-app#build` with change not committed
Do the same thing with the `ls` command
$ ${TURBO} ls --affected
WARNING ls command is experimental and may change in the future
1 package
1 package (npm)

my-app apps[\/\\]my-app (re)

Expand Down Expand Up @@ -56,7 +56,7 @@ Validate that we only run `my-app#build` with change committed
Do the same thing with the `ls` command
$ ${TURBO} ls --affected
WARNING ls command is experimental and may change in the future
1 package
1 package (npm)

my-app apps[\/\\]my-app (re)

Expand All @@ -76,7 +76,7 @@ Override the SCM base to be HEAD, so nothing runs
Do the same thing with the `ls` command
$ TURBO_SCM_BASE="HEAD" ${TURBO} ls --affected
WARNING ls command is experimental and may change in the future
0 packages
0 no packages (npm)



Expand All @@ -96,7 +96,7 @@ Override the SCM head to be main, so nothing runs
Do the same thing with the `ls` command
$ TURBO_SCM_HEAD="main" ${TURBO} ls --affected
WARNING ls command is experimental and may change in the future
0 packages
0 no packages (npm)



Expand Down Expand Up @@ -127,7 +127,7 @@ Run the build and expect only `my-app` to be affected, since between
Do the same thing with the `ls` command
$ ${TURBO} ls --affected
WARNING ls command is experimental and may change in the future
1 package
1 package (npm)

my-app apps[\/\\]my-app (re)

Expand Down Expand Up @@ -167,7 +167,7 @@ Do the same thing with the `ls` command
WARNING ls command is experimental and may change in the future
WARNING unable to detect git range, assuming all files have changed: git error: fatal: main...HEAD: no merge base

3 packages
3 packages (npm)

another packages[\/\\]another (re)
my-app apps[\/\\]my-app (re)
Expand Down
Loading
Loading