diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 80d851f4..9280a3d1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -54,7 +54,7 @@ jobs: matrix: rust: - stable - - 1.88.0 # MSRV + - 1.90.0 # MSRV steps: - name: Setup | Checkout uses: actions/checkout@v5 @@ -171,6 +171,7 @@ jobs: - initializer - leptos - no-rust + - node-module - proxy - seed - target-path diff --git a/Cargo.lock b/Cargo.lock index 86f0604c..e038549b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adler2" @@ -144,6 +144,32 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "async-compression" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-tar" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e9933aa2da420042c67e2ea83eec765919347d9742592744dc97cc42ef20c5d" +dependencies = [ + "filetime", + "futures-core", + "libc", + "redox_syscall 0.7.0", + "tokio", + "tokio-stream", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -526,6 +552,23 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "compression-codecs" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + [[package]] name = "console" version = "0.16.2" @@ -4167,9 +4210,11 @@ dependencies = [ [[package]] name = "trunk" -version = "0.21.14" +version = "0.22.0-beta.1" dependencies = [ "anyhow", + "async-compression", + "async-tar", "axum", "axum-server", "base64", @@ -4222,10 +4267,12 @@ dependencies = [ "tokio", "tokio-stream", "tokio-tungstenite 0.26.2", + "tokio-util", "toml", "tower-http", "tracing", "tracing-subscriber", + "url", "which", "zip", ] diff --git a/Cargo.toml b/Cargo.toml index 0ec7b917..1181cf2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "trunk" -version = "0.21.14" +version = "0.22.0-beta.1" edition = "2024" description = "Build, bundle & ship your Rust WASM application to the web." license = "MIT/Apache-2.0" @@ -12,7 +12,7 @@ repository = "https://github.com/trunk-rs/trunk" readme = "README.md" categories = ["command-line-utilities", "wasm", "web-programming"] keywords = ["wasm", "bundler", "web", "build-tool", "compiler"] -rust-version = "1.88.0" +rust-version = "1.90.0" [profile.release] lto = "fat" @@ -21,6 +21,8 @@ panic = "abort" [dependencies] anyhow = "1" +async-compression = { version = "0.4.41", features = ["gzip", "tokio"] } +async-tar = { version = "0.6", default-features = false, features = ["runtime-tokio"] } axum = { version = "0.8.1", features = ["ws"] } axum-server = "0.7" base64 = "0.22" @@ -53,7 +55,7 @@ oxipng = "9" rand = "0.9.0" regex = "1" remove_dir_all = "1" -reqwest = { version = "0.12", default-features = false, features = ["stream", "trust-dns"] } +reqwest = { version = "0.12", default-features = false, features = ["stream", "trust-dns", "json"] } schemars = { version = "0.8", features = ["derive"] } seahash = { version = "4", features = ["use_std"] } semver = "1" @@ -68,10 +70,12 @@ time = { version = "0.3", features = ["serde-well-known"] } tokio = { version = "1", default-features = false, features = ["full"] } tokio-stream = { version = "0.1", default-features = false, features = ["fs", "sync"] } tokio-tungstenite = "0.26" +tokio-util = "0.7.18" toml = "0.8" tower-http = { version = "0.6.1", features = ["fs", "trace", "set-header"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +url = "2" which = "8" zip = "7" diff --git a/examples/node-module/Cargo.lock b/examples/node-module/Cargo.lock new file mode 100644 index 00000000..7ad23674 --- /dev/null +++ b/examples/node-module/Cargo.lock @@ -0,0 +1,146 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bumpalo" +version = "3.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "node-module-example" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "syn" +version = "2.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] diff --git a/examples/node-module/Cargo.toml b/examples/node-module/Cargo.toml new file mode 100644 index 00000000..f5c56e6a --- /dev/null +++ b/examples/node-module/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "node-module-example" +version = "0.1.0" +authors = ["Marc-Antoine Arnaud "] +edition = "2024" + +[dependencies] +console_error_panic_hook = "0.1" +wasm-bindgen = "0.2" +web-sys = { version = "0.3", features = ["Window", "Document", "HtmlElement", "Node", "Text"] } diff --git a/examples/node-module/Trunk.toml b/examples/node-module/Trunk.toml new file mode 100644 index 00000000..dd87af58 --- /dev/null +++ b/examples/node-module/Trunk.toml @@ -0,0 +1,3 @@ +[[node_packages]] +name = "@patternfly/patternfly" +version = "6.4.0" diff --git a/examples/node-module/index.html b/examples/node-module/index.html new file mode 100644 index 00000000..05730e0c --- /dev/null +++ b/examples/node-module/index.html @@ -0,0 +1,14 @@ + + + + + + Npm dependency + + + + + + + + diff --git a/examples/node-module/src/main.rs b/examples/node-module/src/main.rs new file mode 100644 index 00000000..975109b0 --- /dev/null +++ b/examples/node-module/src/main.rs @@ -0,0 +1,34 @@ +#![recursion_limit = "1024"] + +use console_error_panic_hook::set_once as set_panic_hook; +use web_sys::window; + +fn start_app() { + let document = window() + .and_then(|win| win.document()) + .expect("could not access document"); + let body = document.body().expect("could not access document.body"); + + let button_text_node = document.create_element("span").unwrap(); + button_text_node + .set_attribute("class", "pf-v6-c-button__text") + .unwrap(); + button_text_node.set_text_content(Some("Primary")); + + let button_node = document.create_element("button").unwrap(); + button_node + .set_attribute("class", "pf-v6-c-button pf-m-primary") + .unwrap(); + button_node.set_attribute("type", "button").unwrap(); + button_node + .append_child(button_text_node.as_ref()) + .expect("failed to append button text"); + + body.append_child(button_node.as_ref()) + .expect("failed to append button"); +} + +fn main() { + set_panic_hook(); + start_app(); +} diff --git a/examples/yaml-config/Cargo.lock b/examples/yaml-config/Cargo.lock index e014da59..358edfbd 100644 --- a/examples/yaml-config/Cargo.lock +++ b/examples/yaml-config/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "bumpalo" @@ -145,7 +145,7 @@ dependencies = [ ] [[package]] -name = "yaml-exmaple" +name = "yaml-example" version = "0.1.0" dependencies = [ "console_error_panic_hook", diff --git a/src/config/models/hook.rs b/src/config/models/hook.rs index d912b6e6..f1c9effa 100644 --- a/src/config/models/hook.rs +++ b/src/config/models/hook.rs @@ -29,10 +29,10 @@ impl Hook { if let Some(cfg) = self.overrides.macos.as_ref() { return &cfg.command; } - } else if cfg!(target_os = "linux") { - if let Some(cfg) = self.overrides.linux.as_ref() { - return &cfg.command; - } + } else if cfg!(target_os = "linux") + && let Some(cfg) = self.overrides.linux.as_ref() + { + return &cfg.command; } &self.command @@ -47,10 +47,10 @@ impl Hook { if let Some(cfg) = self.overrides.macos.as_ref() { return &cfg.command_arguments; } - } else if cfg!(target_os = "linux") { - if let Some(cfg) = self.overrides.linux.as_ref() { - return &cfg.command_arguments; - } + } else if cfg!(target_os = "linux") + && let Some(cfg) = self.overrides.linux.as_ref() + { + return &cfg.command_arguments; } &self.command_arguments diff --git a/src/config/models/mod.rs b/src/config/models/mod.rs index a8d74f90..6cee7f72 100644 --- a/src/config/models/mod.rs +++ b/src/config/models/mod.rs @@ -9,6 +9,7 @@ mod build; mod clean; mod core; mod hook; +mod node_package; mod proxy; mod serve; mod tools; @@ -18,6 +19,7 @@ pub use build::*; pub use clean::*; pub use core::*; pub use hook::*; +pub use node_package::*; pub use proxy::*; pub use serve::*; pub use tools::*; @@ -56,6 +58,9 @@ pub struct Configuration { #[serde(default)] pub hooks: Hooks, + #[serde(default)] + pub node_packages: NodePackages, + #[serde(default)] pub watch: Watch, @@ -80,6 +85,7 @@ impl ConfigModel for Configuration { self.tools.migrate()?; self.hooks.migrate()?; + self.node_packages.migrate()?; self.proxies.migrate()?; self.clean.migrate()?; diff --git a/src/config/models/node_package.rs b/src/config/models/node_package.rs new file mode 100644 index 00000000..ddf573a7 --- /dev/null +++ b/src/config/models/node_package.rs @@ -0,0 +1,27 @@ +use crate::config::models::ConfigModel; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// Config options for build system node module. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct NodePackage { + /// Optional npm registry to use (default is https://registry.npmjs.org) + #[serde(default)] + pub registry: Option, + /// Package name in https://npmjs.com + pub name: String, + /// Version of the package + pub version: String, + /// Path where to install the package + #[serde(default)] + pub target_path: Option, +} + +/// New type for handling `Vec` +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize, JsonSchema)] +pub struct NodePackages( + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub Vec, +); + +impl ConfigModel for NodePackages {} diff --git a/src/config/rt/build.rs b/src/config/rt/build.rs index 5fe8f0f9..8b12fb87 100644 --- a/src/config/rt/build.rs +++ b/src/config/rt/build.rs @@ -1,4 +1,5 @@ use super::{super::STAGE_DIR, RtcBuilder}; +use crate::config::models::{NodePackage, NodePackages}; use crate::{ config::{ Hooks, @@ -60,6 +61,8 @@ pub struct RtcBuild { pub cargo_example: Option, /// Configuration for automatic application download. pub tools: Tools, + /// Build process node_package. + pub node_packages: Vec, /// Build process hooks. pub hooks: Vec, /// A bool indicating if the output HTML should have the WebSocket autoloader injected. @@ -120,6 +123,7 @@ impl RtcBuild { core: core_config, build, tools, + node_packages: NodePackages(node_packages), hooks: Hooks(hooks), .. } = config; @@ -199,6 +203,7 @@ impl RtcBuild { cargo_features, cargo_example: build.example, tools, + node_packages, hooks, inject_autoloader, inject_scripts: build.inject_scripts, @@ -244,6 +249,7 @@ impl RtcBuild { cargo_features: Features::All, cargo_example: None, tools: Default::default(), + node_packages: Vec::new(), hooks: Vec::new(), inject_autoloader: true, inject_scripts: true, diff --git a/src/pipelines/html.rs b/src/pipelines/html.rs index 9fa35142..84190c8c 100644 --- a/src/pipelines/html.rs +++ b/src/pipelines/html.rs @@ -1,5 +1,6 @@ //! Source HTML pipelines. +use super::node_packages::{spawn_node_packages, wait_node_packages}; use crate::{ common::{ html_rewrite::{Document, DocumentOptions}, @@ -82,6 +83,8 @@ impl HtmlPipeline { async fn run(self: Arc) -> Result<()> { tracing::debug!("spawning asset pipelines"); + wait_node_packages(spawn_node_packages(self.cfg.clone())).await?; + // Spawn and wait on pre-build hooks. wait_hooks(spawn_hooks(self.cfg.clone(), PipelineStage::PreBuild)).await?; diff --git a/src/pipelines/mod.rs b/src/pipelines/mod.rs index 96461b82..e192d84b 100644 --- a/src/pipelines/mod.rs +++ b/src/pipelines/mod.rs @@ -9,6 +9,7 @@ mod html; mod icon; mod inline; mod js; +mod node_packages; mod rust; mod sass; mod tailwind_css; @@ -172,18 +173,20 @@ impl TrunkAsset { /// Spawn the build pipeline for this asset. pub fn spawn(self) -> JoinHandle> { - match self { - Self::Css(inner) => inner.spawn(), - Self::Sass(inner) => inner.spawn(), - Self::TailwindCss(inner) => inner.spawn(), - Self::TailwindCssExtra(inner) => inner.spawn(), - Self::Js(inner) => inner.spawn(), - Self::Icon(inner) => inner.spawn(), - Self::Inline(inner) => inner.spawn(), - Self::CopyFile(inner) => inner.spawn(), - Self::CopyDir(inner) => inner.spawn(), - Self::RustApp(inner) => inner.spawn(), - } + let (join_handle, a) = match self { + Self::Css(inner) => (inner.spawn(), "Css"), + Self::Sass(inner) => (inner.spawn(), "Sass"), + Self::TailwindCss(inner) => (inner.spawn(), "TailwindCss"), + Self::TailwindCssExtra(inner) => (inner.spawn(), "TailwindCssExtra"), + Self::Js(inner) => (inner.spawn(), "Js"), + Self::Icon(inner) => (inner.spawn(), "Icon"), + Self::Inline(inner) => (inner.spawn(), "Inline"), + Self::CopyFile(inner) => (inner.spawn(), "CopyFile"), + Self::CopyDir(inner) => (inner.spawn(), "CopyDir"), + Self::RustApp(inner) => (inner.spawn(), "RustApp"), + }; + tracing::warn!("spawning pipeline for {a:?}"); + join_handle } } diff --git a/src/pipelines/node_packages/get_package_error.rs b/src/pipelines/node_packages/get_package_error.rs new file mode 100644 index 00000000..b2b115e8 --- /dev/null +++ b/src/pipelines/node_packages/get_package_error.rs @@ -0,0 +1,11 @@ +use thiserror::Error; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Error)] +pub enum GetPackageError { + #[error("failed to send `GetPackage` request: {0}")] + Send(String), + #[error("failed to receive `GetPackage` response: {0}")] + Receive(String), + #[error("node package not found")] + NotFound, +} diff --git a/src/pipelines/node_packages/mod.rs b/src/pipelines/node_packages/mod.rs new file mode 100644 index 00000000..e68ae65e --- /dev/null +++ b/src/pipelines/node_packages/mod.rs @@ -0,0 +1,109 @@ +mod get_package_error; +mod node_package_client; +mod node_package_information; + +use crate::{ + common::copy_dir_recursive, config::rt::RtcBuild, + pipelines::node_packages::node_package_client::NodePackageClient, +}; +use anyhow::Context; +use async_compression::tokio::bufread::GzipDecoder; +use futures_util::{StreamExt, TryStreamExt, stream::FuturesUnordered}; +use std::{io, path::PathBuf, sync::Arc}; +use tokio::{fs::remove_dir_all, task::JoinHandle}; +use tokio_util::io::StreamReader; + +/// A `FuturesUnordered` containing a `JoinHandle` for each hook-running task. +pub type NodePackageHandles = FuturesUnordered>>; +pub fn spawn_node_packages(cfg: Arc) -> NodePackageHandles { + tracing::info!("node packages {:?}", cfg.node_packages); + + let futures: FuturesUnordered<_> = cfg + .node_packages + .iter() + .map(|node_package_cfg| { + let package_information = format!( + "{}@{}{}", + node_package_cfg.name, + node_package_cfg.version, + node_package_cfg + .registry + .clone() + .map(|registry| format!("(registry: {registry})")) + .unwrap_or_default() + ); + + tracing::info!("download node package {package_information}"); + + let node_package_cfg = node_package_cfg.clone(); + + tokio::spawn(async move { + let http_node_module_client = if let Some(registry) = node_package_cfg.registry { + NodePackageClient::new(®istry)? + } else { + NodePackageClient::default() + }; + + let target_path = node_package_cfg.target_path.unwrap_or(format!( + "target/node_modules/{}/{}", + node_package_cfg.name, node_package_cfg.version + )); + let target_path = PathBuf::from(target_path); + + if target_path.exists() { + tracing::debug!( + "target path ({}) already exists, skipping", + target_path.display() + ); + return Ok(()); + } + + let package = http_node_module_client + .get_package(&node_package_cfg.name, &node_package_cfg.version) + .await + .with_context(|| { + format!("failed to retrieve Node package: {package_information}") + })?; + + // request + + let tarball = reqwest::get(package.distribution.tarball) + .await? + .bytes_stream(); + + // unpack + + let tarball = tarball.map_err(io::Error::other); + let archive_data = GzipDecoder::new(StreamReader::new(tarball)); + let archive = async_tar::Archive::new(archive_data); + archive.unpack(&target_path).await?; + + // move + + let package_directory = target_path.join("package"); + + tracing::debug!("move from {package_directory:?} to {target_path:?}"); + + copy_dir_recursive(package_directory.clone(), target_path).await?; + remove_dir_all(package_directory).await?; + + // done + + tracing::info!("finished to download node package {package_information}"); + + Ok(()) + }) + }) + .collect(); + + futures +} + +/// Waits for all the given hooks to finish. +pub async fn wait_node_packages(mut futures: NodePackageHandles) -> anyhow::Result<()> { + while let Some(result) = futures.next().await { + result??; + } + + Ok(()) +} diff --git a/src/pipelines/node_packages/node_package_client.rs b/src/pipelines/node_packages/node_package_client.rs new file mode 100644 index 00000000..43f08a09 --- /dev/null +++ b/src/pipelines/node_packages/node_package_client.rs @@ -0,0 +1,75 @@ +use super::{get_package_error::GetPackageError, node_package_information::NodePackageInformation}; +use crate::version::USER_AGENT; +use anyhow::Result; +use reqwest::{Client, StatusCode}; +use url::Url; + +/// A client for npmjs.org compatible registries +pub struct NodePackageClient { + client: Client, + server: Url, +} + +impl NodePackageClient { + pub fn new(url: &str) -> Result { + let client = Client::builder().user_agent(USER_AGENT).build()?; + + let server = Url::parse(url).map_err(|error| { + std::io::Error::other(format!("the npm registry URL is well-formed: {error}")) + })?; + + Ok(Self { client, server }) + } + + pub fn api_url(&self, segments: I) -> Url + where + I: IntoIterator, + I::Item: AsRef, + { + let mut res = self.server.clone(); + + if let Ok(mut p) = res.path_segments_mut() { + p.extend(segments); + } + + res + } +} + +impl Default for NodePackageClient { + #![allow(clippy::expect_used)] + fn default() -> Self { + Self::new("https://registry.npmjs.org/").expect("The NPMJS URL must parse") + } +} + +impl NodePackageClient { + pub async fn get_package( + &self, + package_name: &str, + package_version: &str, + ) -> Result { + let url = self.api_url([package_name, package_version]); + + let res = self + .client + .get(url) + .send() + .await + .map_err(|e| GetPackageError::Send(format!("{e:?}")))?; + + match res.status() { + StatusCode::OK => { + let body: NodePackageInformation = res + .json() + .await + .map_err(|e| GetPackageError::Receive(format!("{e:?}")))?; + Ok(body) + } + StatusCode::NOT_FOUND => Err(GetPackageError::NotFound), + code => Err(GetPackageError::Receive(format!( + "unexpected status code {code}" + ))), + } + } +} diff --git a/src/pipelines/node_packages/node_package_information.rs b/src/pipelines/node_packages/node_package_information.rs new file mode 100644 index 00000000..5c4ac8e8 --- /dev/null +++ b/src/pipelines/node_packages/node_package_information.rs @@ -0,0 +1,16 @@ +#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)] +pub struct NodePackageInformation { + pub name: String, + pub version: String, + #[serde(rename = "dist")] + pub distribution: NodePackageDistribution, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)] +pub struct NodePackageDistribution { + pub shasum: String, + pub tarball: String, + #[serde(rename = "fileCount")] + pub file_count: usize, + pub integrity: String, +} diff --git a/src/pipelines/rust/mod.rs b/src/pipelines/rust/mod.rs index 2fb1c4aa..8a00e383 100644 --- a/src/pipelines/rust/mod.rs +++ b/src/pipelines/rust/mod.rs @@ -457,18 +457,17 @@ impl RustApp { // Send cargo's target dir over to the watcher to be ignored. We must do this before // checking for errors, otherwise the dir will never be ignored. If we attempt to do // this pre-build, the canonicalization will fail and will not be ignored. - if let Some(chan) = &mut self.ignore_chan { - if let Ok(target_dir) = self + if let Some(chan) = &mut self.ignore_chan + && let Ok(target_dir) = self .manifest .metadata .target_directory .clone() .into_std_path_buf() .canonicalize() - { - let target_dir_recursive = target_dir.join("**"); - let _ = chan.try_send(vec![target_dir, target_dir_recursive]); - } + { + let target_dir_recursive = target_dir.join("**"); + let _ = chan.try_send(vec![target_dir, target_dir_recursive]); } // Now propagate any errors which came from the cargo build. @@ -858,10 +857,10 @@ impl RustApp { } // if we have a target name - if let Some(target_name) = &self.target_name { - if target_name != &art.target.name { - return false; - } + if let Some(target_name) = &self.target_name + && target_name != &art.target.name + { + return false; } true diff --git a/src/serve/mod.rs b/src/serve/mod.rs index 55f65294..31c59987 100644 --- a/src/serve/mod.rs +++ b/src/serve/mod.rs @@ -98,10 +98,11 @@ impl ServeSystem { .await?; // Open the browser. - if self.cfg.open && build_res.is_ok() { - if let Err(err) = open::that(self.open_http_addr) { - tracing::error!(error = ?err, "error opening browser"); - } + if self.cfg.open + && build_res.is_ok() + && let Err(err) = open::that(self.open_http_addr) + { + tracing::error!(error = ?err, "error opening browser"); } drop(self.shutdown_tx); // Drop the broadcast channel to ensure it does not keep the system alive. diff --git a/src/version/enabled/mod.rs b/src/version/enabled/mod.rs index 6a340e05..9e63e92b 100644 --- a/src/version/enabled/mod.rs +++ b/src/version/enabled/mod.rs @@ -1,5 +1,5 @@ use crate::version::{ - NAME, VERSION, + NAME, USER_AGENT, VERSION, enabled::state::{State, Versions}, }; use semver::Version; @@ -83,8 +83,7 @@ fn announce_version(versions: &Versions) { async fn most_recent() -> anyhow::Result { tracing::debug!("Checking for updates"); - let client = - crates_io_api::AsyncClient::new(&format!("{NAME}/{VERSION}"), Duration::from_secs(1))?; + let client = crates_io_api::AsyncClient::new(USER_AGENT, Duration::from_secs(1))?; let response = client.get_crate(NAME).await?; let versions = response diff --git a/src/version/enabled/state.rs b/src/version/enabled/state.rs index a8b70e2a..deb7f24a 100644 --- a/src/version/enabled/state.rs +++ b/src/version/enabled/state.rs @@ -96,14 +96,14 @@ pub async fn record_checked(versions: Versions) { } }; - if let Some(parent) = file.parent() { - if let Err(err) = tokio::fs::create_dir_all(parent).await { - tracing::debug!( - "Failed to create parent directory for update state ({}): {err}", - parent.display() - ); - return; - } + if let Some(parent) = file.parent() + && let Err(err) = tokio::fs::create_dir_all(parent).await + { + tracing::debug!( + "Failed to create parent directory for update state ({}): {err}", + parent.display() + ); + return; } if let Err(err) = tokio::fs::write(&file, state).await { diff --git a/src/version/mod.rs b/src/version/mod.rs index 136123a1..24bfd59a 100644 --- a/src/version/mod.rs +++ b/src/version/mod.rs @@ -14,4 +14,6 @@ pub(crate) use enforce::enforce_version_with; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); #[cfg(feature = "update_check")] -const NAME: &str = env!("CARGO_PKG_NAME"); +pub const NAME: &str = env!("CARGO_PKG_NAME"); + +pub const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); diff --git a/src/ws.rs b/src/ws.rs index 722355ed..7ab213b3 100644 --- a/src/ws.rs +++ b/src/ws.rs @@ -82,14 +82,12 @@ pub(crate) async fn handle_ws(mut ws: WebSocket, state: Arc) { tracing::trace!("Message to send: {msg:?}"); - if let Some(msg) = msg { - if let Ok(text) = serde_json::to_string(&msg) { - if let Err(err) = ws.send(Message::Text(text.into())).await { + if let Some(msg) = msg + && let Ok(text) = serde_json::to_string(&msg) + && let Err(err) = ws.send(Message::Text(text.into())).await { tracing::info!("autoload websocket failed to send: {err}"); break; } - } - } } } }