Skip to content

Commit

Permalink
feat(ls): support output format (#9031)
Browse files Browse the repository at this point in the history
### Description

Support output format for `ls`

### Testing Instructions

<!--
  Give a quick description of steps to test your changes.
-->
  • Loading branch information
tknickman committed Aug 19, 2024
1 parent d05c5f0 commit 452a916
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 30 deletions.
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

0 comments on commit 452a916

Please sign in to comment.