diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4acde149ca..ceb2570a33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -152,6 +152,13 @@ jobs: - uses: actions/checkout@v4 - uses: taiki-e/install-action@cargo-llvm-cov + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: "20.11.0" + - name: Install Prettier + run: npm i -g prettier + - name: Install Stable Toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 with: diff --git a/Cargo.lock b/Cargo.lock index 1b5d6e8a84..34ffb82b77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5140,6 +5140,7 @@ dependencies = [ "serde_yaml", "stripmargin", "strum_macros 0.26.2", + "tailcall-prettier", "temp-env", "tempfile", "thiserror", @@ -5209,6 +5210,16 @@ dependencies = [ "worker", ] +[[package]] +name = "tailcall-prettier" +version = "0.1.0" +dependencies = [ + "anyhow", + "lazy_static", + "strum_macros 0.26.2", + "tokio", +] + [[package]] name = "tailcall_query_plan" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index cd9b7ffada..16108c2d8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -148,6 +148,7 @@ tonic-types = "0.11.0" [dev-dependencies] +tailcall-prettier = {path = "tailcall-prettier"} criterion = "0.5.1" httpmock = "0.7.0" pretty_assertions = "1.4.0" @@ -196,6 +197,7 @@ members = [ "tailcall-autogen", "tailcall-aws-lambda", "tailcall-cloudflare", + "tailcall-prettier", "tailcall-query-plan", ] diff --git a/tailcall-prettier/Cargo.toml b/tailcall-prettier/Cargo.toml new file mode 100644 index 0000000000..8916f0eaac --- /dev/null +++ b/tailcall-prettier/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "tailcall-prettier" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.82" +lazy_static = "1.4.0" +strum_macros = "0.26.2" +tokio.workspace = true diff --git a/tailcall-prettier/src/lib.rs b/tailcall-prettier/src/lib.rs new file mode 100644 index 0000000000..13b706a359 --- /dev/null +++ b/tailcall-prettier/src/lib.rs @@ -0,0 +1,26 @@ +use std::sync::Arc; +mod parser; +mod prettier; +use anyhow::Result; +pub use parser::Parser; +use prettier::Prettier; + +lazy_static::lazy_static! { + static ref PRETTIER: Arc = Arc::new(Prettier::new()); +} + +pub async fn format>(source: T, parser: Parser) -> Result { + PRETTIER.format(source.as_ref().to_string(), parser).await +} + +#[cfg(test)] +mod tests { + use crate::{format, Parser}; + + #[tokio::test] + async fn test_js() -> anyhow::Result<()> { + let prettier = format("const x={a:3};", Parser::Js).await?; + assert_eq!("const x = {a: 3}\n", prettier); + Ok(()) + } +} diff --git a/tailcall-prettier/src/parser.rs b/tailcall-prettier/src/parser.rs new file mode 100644 index 0000000000..1a0484983f --- /dev/null +++ b/tailcall-prettier/src/parser.rs @@ -0,0 +1,30 @@ +use anyhow::{anyhow, Result}; + +#[derive(strum_macros::Display)] +pub enum Parser { + Gql, + Yml, + Json, + Md, + Ts, + Js, +} + +impl Parser { + pub fn detect(path: &str) -> Result { + let ext = path + .split('.') + .last() + .ok_or(anyhow!("No file extension found"))? + .to_lowercase(); + match ext.as_str() { + "gql" | "graphql" => Ok(Parser::Gql), + "yml" | "yaml" => Ok(Parser::Yml), + "json" => Ok(Parser::Json), + "md" => Ok(Parser::Md), + "ts" => Ok(Parser::Ts), + "js" => Ok(Parser::Js), + _ => Err(anyhow!("Unsupported file type")), + } + } +} diff --git a/tailcall-prettier/src/prettier.rs b/tailcall-prettier/src/prettier.rs new file mode 100644 index 0000000000..0693ae6b67 --- /dev/null +++ b/tailcall-prettier/src/prettier.rs @@ -0,0 +1,54 @@ +use std::io::Write; +use std::process::{Command, Stdio}; + +use anyhow::{anyhow, Result}; + +pub use super::Parser; + +pub struct Prettier { + runtime: tokio::runtime::Runtime, +} + +impl Prettier { + pub fn new() -> Prettier { + let runtime = tokio::runtime::Builder::new_multi_thread() + .max_blocking_threads(1024) + .build() + .unwrap(); + + Self { runtime } + } + + pub async fn format(&self, source: String, parser: Parser) -> Result { + self.runtime + .spawn_blocking(move || { + let mut command = command(); + let mut child = command + .arg("--stdin-filepath") + .arg(format!("file.{}", parser)) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn()?; + + if let Some(ref mut stdin) = child.stdin { + stdin.write_all(source.as_bytes())?; + } + + let output = child.wait_with_output()?; + if output.status.success() { + Ok(String::from_utf8(output.stdout)?) + } else { + Err(anyhow!("Prettier formatting failed")) + } + }) + .await? + } +} + +fn command() -> Command { + if cfg!(target_os = "windows") { + Command::new("prettier.cmd") + } else { + Command::new("prettier") + } +} diff --git a/tests/execution_spec.rs b/tests/execution_spec.rs index 3c542f2c5a..ef2edccd52 100644 --- a/tests/execution_spec.rs +++ b/tests/execution_spec.rs @@ -889,9 +889,25 @@ async fn assert_spec(spec: ExecutionSpec, opentelemetry: &InMemoryTelemetry) { // \r is added automatically in windows, it's safe to replace it with \n let content = content.replace("\r\n", "\n"); + let path_str = spec.path.display().to_string(); + + let identity = tailcall_prettier::format( + identity, + tailcall_prettier::Parser::detect(path_str.as_str()).unwrap(), + ) + .await + .unwrap(); + + let content = tailcall_prettier::format( + content, + tailcall_prettier::Parser::detect(path_str.as_str()).unwrap(), + ) + .await + .unwrap(); + pretty_assertions::assert_eq!( identity, - content.as_ref(), + content, "Identity check failed for {:#?}", spec.path, );