diff --git a/Trunk.toml b/Trunk.toml index ddc9cc8d..300dae55 100644 --- a/Trunk.toml +++ b/Trunk.toml @@ -22,8 +22,8 @@ offline = false frozen = false # Require Cargo.lock is up to date locked = false -# Allow disabling minification -no_minification = false +# Control minification +minify = "never" # can be one of: never, on_release, always # Allow disabling sub-resource integrity (SRI) no_sri = false @@ -45,7 +45,7 @@ no_autoreload = false # Disable error reporting no_error_reporting = false # Additional headers set for responses. -#headers = { "test-header" = "header value", "test-header2" = "header value 2" } +# headers = { "test-header" = "header value", "test-header2" = "header value 2" } # Protocol used for autoreload WebSockets connection. ws_protocol = "ws" # The certificate/private key pair to use for TLS, which is enabled if both are set. diff --git a/site/content/assets.md b/site/content/assets.md index b463c7f9..20a7b28a 100644 --- a/site/content/assets.md +++ b/site/content/assets.md @@ -56,7 +56,7 @@ This will typically look like: `` tag instead of using a `` tag. - `data-integrity`: (optional) the `integrity` digest type for code & script resources. Defaults to plain `sha384`. + - `data-no-minify`: (optional) Opt-out of minification. Also see: [Minification](#minification). - `data-target-path`: (optional) Path where the output is placed inside the dist dir. If not present, the directory is placed in the dist root. The path must be a relative path without `..`. ## icon @@ -72,6 +73,7 @@ This will typically look like: `, - /// Allows disabling minification + /// Enable minification. + /// + /// This overrides the value from the configuration file. + // cli version #[serde(default)] - #[arg(long)] - pub no_minification: bool, + #[arg(id = "minify", short = 'M', long)] + pub minify_cli: bool, + + /// Control minification. + // toml version + #[serde(default, skip_serializing_if = "Option::is_none")] + #[serde(rename = "minify")] + #[arg(skip)] + pub minify_toml: Option, /// Allows disabling sub-resource integrity (SRI) #[serde(default)] diff --git a/src/config/models/mod.rs b/src/config/models/mod.rs index 05c56063..9c90b41d 100644 --- a/src/config/models/mod.rs +++ b/src/config/models/mod.rs @@ -353,9 +353,10 @@ impl ConfigOpts { g.pattern_preload = g.pattern_preload.or(l.pattern_preload); g.pattern_script = g.pattern_script.or(l.pattern_script); g.pattern_params = g.pattern_params.or(l.pattern_params); - // NOTE: this can not be disabled in the cascade. - if l.no_minification { - g.no_minification = true; + + g.minify_toml = g.minify_toml.or(l.minify_toml); + if l.minify_cli { + g.minify_cli = true; } // NOTE: this can not be disabled in the cascade. if l.no_sri { diff --git a/src/config/models/types/duration.rs b/src/config/models/types/duration.rs index d3e3de64..296b26c7 100644 --- a/src/config/models/types/duration.rs +++ b/src/config/models/types/duration.rs @@ -2,11 +2,12 @@ use serde::{Deserialize, Deserializer}; use std::str::FromStr; use std::time::Duration; +/// A newtype to allow using humantime durations as clap and serde values. #[derive(Clone, Debug)] pub struct ConfigDuration(pub Duration); impl<'de> Deserialize<'de> for ConfigDuration { - fn deserialize(deserializer: D) -> std::result::Result + fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { @@ -17,7 +18,7 @@ impl<'de> Deserialize<'de> for ConfigDuration { impl FromStr for ConfigDuration { type Err = humantime::DurationError; - fn from_str(s: &str) -> std::result::Result { + fn from_str(s: &str) -> Result { Ok(Self(humantime::Duration::from_str(s)?.into())) } } diff --git a/src/config/models/types/minify.rs b/src/config/models/types/minify.rs new file mode 100644 index 00000000..a774c777 --- /dev/null +++ b/src/config/models/types/minify.rs @@ -0,0 +1,11 @@ +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum Minify { + /// Never minify + #[default] + Never, + /// Minify for release builds + OnRelease, + /// Minify for all builds + Always, +} diff --git a/src/config/models/types/mod.rs b/src/config/models/types/mod.rs index cc066dab..4cc10dce 100644 --- a/src/config/models/types/mod.rs +++ b/src/config/models/types/mod.rs @@ -1,9 +1,11 @@ mod address_family; mod base_url; mod duration; +mod minify; mod ws; pub use address_family::*; pub use base_url::*; pub use duration::*; +pub use minify::*; pub use ws::*; diff --git a/src/config/rt/build.rs b/src/config/rt/build.rs index 2978c327..9ae4f075 100644 --- a/src/config/rt/build.rs +++ b/src/config/rt/build.rs @@ -1,6 +1,8 @@ use super::super::{DIST_DIR, STAGE_DIR}; -use crate::config::models::BaseUrl; -use crate::config::{ConfigOptsBuild, ConfigOptsCore, ConfigOptsHook, ConfigOptsTools, RtcCore}; +use crate::config::{ + models::{BaseUrl, Minify}, + ConfigOptsBuild, ConfigOptsCore, ConfigOptsHook, ConfigOptsTools, RtcCore, +}; use anyhow::{ensure, Context}; use std::collections::HashMap; use std::io::ErrorKind; @@ -72,8 +74,8 @@ pub struct RtcBuild { /// /// **WARNING**: Setting this to true can make you vulnerable to man-in-the-middle attacks. Sometimes this is necessary when working behind corporate proxies. pub accept_invalid_certs: Option, - /// Allow disabling minification - pub no_minification: bool, + /// Control minification + pub minify: Minify, /// Allow disabling SRI pub no_sri: bool, /// Ignore error's due to self closed script tags, instead will issue a warning. @@ -152,6 +154,13 @@ impl RtcBuild { public_url = public_url.fix_trailing_slash(); } + let minify = match (opts.minify_cli, opts.minify_toml) { + // the CLI will override with "always" + (true, _) => Minify::Always, + // otherwise, we take the configuration value, or the default + (false, minify) => minify.unwrap_or_default(), + }; + Ok(Self { core, target, @@ -174,7 +183,7 @@ impl RtcBuild { locked: opts.locked, root_certificate: opts.root_certificate.map(PathBuf::from), accept_invalid_certs: opts.accept_invalid_certs, - no_minification: opts.no_minification, + minify, no_sri: opts.no_sri, ignore_script_error: opts.ignore_script_error, }) @@ -217,16 +226,23 @@ impl RtcBuild { locked: false, root_certificate: None, accept_invalid_certs: None, - no_minification: false, + minify: Minify::Never, no_sri: false, ignore_script_error: false, }) } - /// Check if minification is globally enabled - /// - /// An asset might have an additional configuration, overriding the global one. + /// Evaluate the minify state with an asset's no_minify setting. + pub fn minify_asset(&self, no_minify: bool) -> bool { + !no_minify && self.should_minify() + } + + /// Evaluate a global minify state, assets might override this. pub fn should_minify(&self) -> bool { - self.release && !self.no_minification + match (self.minify, self.release) { + (Minify::Never, _) => false, + (Minify::OnRelease, release) => release, + (Minify::Always, _) => true, + } } } diff --git a/src/pipelines/css.rs b/src/pipelines/css.rs index 7a92e80b..402a0001 100644 --- a/src/pipelines/css.rs +++ b/src/pipelines/css.rs @@ -28,7 +28,7 @@ pub struct Css { /// The required integrity setting integrity: IntegrityType, /// Whether to minify or not - minify: bool, + no_minify: bool, /// Optional target path inside the dist dir. target_path: Option, } @@ -51,7 +51,7 @@ impl Css { let asset = AssetFile::new(&html_dir, path).await?; let integrity = IntegrityType::from_attrs(&attrs, &cfg)?; - let minify = !attrs.contains_key(ATTR_NO_MINIFY); + let no_minify = attrs.contains_key(ATTR_NO_MINIFY); let target_path = data_target_path(&attrs)?; Ok(Self { @@ -60,7 +60,7 @@ impl Css { asset, attrs, integrity, - minify, + no_minify, target_path, }) } @@ -76,7 +76,6 @@ impl Css { async fn run(self) -> Result { let rel_path = crate::common::strip_prefix(&self.asset.path); tracing::debug!(path = ?rel_path, "copying & hashing css"); - let minify = self.cfg.should_minify() && self.minify; let result_path = target_path(&self.cfg.staging_dist, self.target_path.as_deref(), None).await?; @@ -87,7 +86,7 @@ impl Css { &self.cfg.staging_dist, &result_path, self.cfg.filehash, - minify, + self.cfg.minify_asset(self.no_minify), AssetFileType::Css, ) .await?; diff --git a/src/pipelines/icon.rs b/src/pipelines/icon.rs index 16b67905..9ea00e34 100644 --- a/src/pipelines/icon.rs +++ b/src/pipelines/icon.rs @@ -27,7 +27,7 @@ pub struct Icon { /// The required integrity setting integrity: IntegrityType, /// Whether to minify or not - minify: bool, + no_minify: bool, /// Optional target path inside the dist dir. target_path: Option, } @@ -50,7 +50,7 @@ impl Icon { let asset = AssetFile::new(&html_dir, path).await?; let integrity = IntegrityType::from_attrs(&attrs, &cfg)?; - let minify = !attrs.contains_key(ATTR_NO_MINIFY); + let no_minify = attrs.contains_key(ATTR_NO_MINIFY); let target_path = data_target_path(&attrs)?; Ok(Self { @@ -58,7 +58,7 @@ impl Icon { cfg, asset, integrity, - minify, + no_minify, target_path, }) } @@ -89,7 +89,7 @@ impl Icon { &self.cfg.staging_dist, &result_dir, self.cfg.filehash, - self.cfg.should_minify() && self.minify, + self.cfg.minify_asset(self.no_minify), AssetFileType::Icon(image_type), ) .await?; diff --git a/src/pipelines/js.rs b/src/pipelines/js.rs index 7927dde1..3a4f8450 100644 --- a/src/pipelines/js.rs +++ b/src/pipelines/js.rs @@ -30,7 +30,7 @@ pub struct Js { /// If it's a JavaScript module (vs a classic script) module: bool, /// Whether to minify or not - minify: bool, + no_minify: bool, /// Optional target path inside the dist dir. target_path: Option, } @@ -52,7 +52,7 @@ impl Js { let integrity = IntegrityType::from_attrs(&attrs, &cfg)?; let module = attrs.get("type").map(|s| s.as_str()) == Some("module"); - let minify = !attrs.contains_key(ATTR_NO_MINIFY); + let no_minify = attrs.contains_key(ATTR_NO_MINIFY); let target_path = data_target_path(&attrs)?; Ok(Self { @@ -62,7 +62,7 @@ impl Js { module, attrs, integrity, - minify, + no_minify, target_path, }) } @@ -78,7 +78,6 @@ impl Js { async fn run(self) -> Result { let rel_path = crate::common::strip_prefix(&self.asset.path); tracing::debug!(path = ?rel_path, "copying & hashing js"); - let minify = self.cfg.should_minify() && self.minify; let result_dir = target_path(&self.cfg.staging_dist, self.target_path.as_deref(), None).await?; @@ -89,7 +88,7 @@ impl Js { &self.cfg.staging_dist, &result_dir, self.cfg.filehash, - minify, + self.cfg.minify_asset(self.no_minify), if self.module { AssetFileType::Mjs } else { diff --git a/src/pipelines/mod.rs b/src/pipelines/mod.rs index 10e45e0c..6255a9d0 100644 --- a/src/pipelines/mod.rs +++ b/src/pipelines/mod.rs @@ -48,6 +48,7 @@ const ATTR_TYPE: &str = "type"; const ATTR_REL: &str = "rel"; const ATTR_NO_MINIFY: &str = "data-no-minify"; const ATTR_TARGET_PATH: &str = "data-target-path"; + const SNIPPETS_DIR: &str = "snippets"; const TRUNK_ID: &str = "data-trunk-id"; const PNG_OPTIMIZATION_LEVEL: u8 = 6; diff --git a/src/pipelines/rust/mod.rs b/src/pipelines/rust/mod.rs index 817651cd..0902f9f8 100644 --- a/src/pipelines/rust/mod.rs +++ b/src/pipelines/rust/mod.rs @@ -800,7 +800,7 @@ impl RustApp { .await .context("error reading JS loader file")?; - let write_bytes = match self.cfg.release && !self.cfg.no_minification { + let write_bytes = match self.cfg.should_minify() { true => minify_js(bytes, mode), false => bytes, }; diff --git a/src/pipelines/sass.rs b/src/pipelines/sass.rs index 917d4a9a..82307e9f 100644 --- a/src/pipelines/sass.rs +++ b/src/pipelines/sass.rs @@ -31,7 +31,7 @@ pub struct Sass { /// The required integrity setting integrity: IntegrityType, /// Whether to minify or not - minify: bool, + no_minify: bool, /// Optional target path inside the dist dir. target_path: Option, } @@ -54,7 +54,7 @@ impl Sass { path.extend(href_attr.split('/')); let asset = AssetFile::new(&html_dir, path).await?; let use_inline = attrs.contains_key(ATTR_INLINE); - let minify = !attrs.contains_key(ATTR_NO_MINIFY); + let no_minify = attrs.contains_key(ATTR_NO_MINIFY); let integrity = IntegrityType::from_attrs(&attrs, &cfg)?; let target_path = data_target_path(&attrs)?; @@ -66,7 +66,7 @@ impl Sass { use_inline, other_attrs: attrs, integrity, - minify, + no_minify, target_path, }) } @@ -105,17 +105,22 @@ impl Sass { .display() .to_string(); - let (source_map, output_style) = match self.cfg.release { - true => ( - "--no-source-map", - match self.cfg.no_minification || !self.minify { - true => "expanded", - false => "compressed", - }, - ), - false => ("--embed-source-map", "expanded"), + // source map setting, embedded for non-release builds + + let source_map = match self.cfg.release { + true => "--no-source-map", + false => "--embed-source-map", }; + // put style, depends on minify state + + let output_style = match self.cfg.minify_asset(self.no_minify) { + true => "compressed", + false => "expanded", + }; + + // collect arguments + let args = &[ source_map, "--style", @@ -124,6 +129,8 @@ impl Sass { &temp_target_file_path, ]; + // run + let rel_path = common::strip_prefix(&self.asset.path); tracing::debug!(path = ?rel_path, "compiling sass/scss"); common::run_command(Application::Sass.name(), &sass, args).await?; diff --git a/src/pipelines/tailwind_css.rs b/src/pipelines/tailwind_css.rs index 3fa17472..bc11243c 100644 --- a/src/pipelines/tailwind_css.rs +++ b/src/pipelines/tailwind_css.rs @@ -4,10 +4,12 @@ use super::{ data_target_path, AssetFile, AttrWriter, Attrs, TrunkAssetPipelineOutput, ATTR_HREF, ATTR_INLINE, ATTR_NO_MINIFY, }; -use crate::common::{self, dist_relative, html_rewrite::Document, target_path}; -use crate::config::RtcBuild; -use crate::processing::integrity::{IntegrityType, OutputDigest}; -use crate::tools::{self, Application}; +use crate::{ + common::{self, dist_relative, html_rewrite::Document, target_path}, + config::RtcBuild, + processing::integrity::{IntegrityType, OutputDigest}, + tools::{self, Application}, +}; use anyhow::{Context, Result}; use std::path::PathBuf; use std::sync::Arc; @@ -29,7 +31,7 @@ pub struct TailwindCss { /// The required integrity setting integrity: IntegrityType, /// Whether to minify or not - minify: bool, + no_minify: bool, /// Optional target path inside the dist dir. target_path: Option, } @@ -51,9 +53,9 @@ impl TailwindCss { path.extend(href_attr.split('/')); let asset = AssetFile::new(&html_dir, path).await?; let use_inline = attrs.contains_key(ATTR_INLINE); - let minify = !attrs.contains_key(ATTR_NO_MINIFY); let integrity = IntegrityType::from_attrs(&attrs, &cfg)?; + let no_minify = attrs.contains_key(ATTR_NO_MINIFY); let target_path = data_target_path(&attrs)?; Ok(Self { @@ -63,7 +65,7 @@ impl TailwindCss { use_inline, integrity, attrs, - minify, + no_minify, target_path, }) } @@ -90,18 +92,21 @@ impl TailwindCss { .await?; // Compile the target tailwind css file. - let minify = self.cfg.should_minify() && self.minify; - let style = if minify { "--minify" } else { "" }; let path_str = dunce::simplified(&self.asset.path).display().to_string(); let file_name = format!("{}.css", &self.asset.file_stem.to_string_lossy()); let file_path = dunce::simplified(&self.cfg.staging_dist.join(&file_name)) .display() .to_string(); - let args = &["--input", &path_str, "--output", &file_path, style]; - let rel_path = crate::common::strip_prefix(&self.asset.path); + let mut args = vec!["--input", &path_str, "--output", &file_path]; + + if self.cfg.minify_asset(self.no_minify) { + args.push("--minify"); + } + + let rel_path = common::strip_prefix(&self.asset.path); tracing::debug!(path = ?rel_path, "compiling tailwind css"); - common::run_command(Application::TailwindCss.name(), &tailwind, args).await?; + common::run_command(Application::TailwindCss.name(), &tailwind, &args).await?; let css = fs::read_to_string(&file_path).await?; fs::remove_file(&file_path).await?;