From 4b68c742d0b13dcc108c180756ecf444cde63273 Mon Sep 17 00:00:00 2001
From: Antoine Busch <antoine.busch@gmail.com>
Date: Wed, 20 Dec 2023 18:05:25 +1100
Subject: [PATCH] feat: Allow disabling CSS and JS minification per file

See #651
---
 examples/vanilla/index.html           |  1 +
 examples/vanilla/src/not_minified.css |  4 ++++
 site/content/assets.md                |  3 +++
 src/pipelines/css.rs                  | 10 ++++++++--
 src/pipelines/js.rs                   | 18 ++++++++----------
 src/pipelines/mod.rs                  | 15 +++++++++++++--
 6 files changed, 37 insertions(+), 14 deletions(-)
 create mode 100644 examples/vanilla/src/not_minified.css

diff --git a/examples/vanilla/index.html b/examples/vanilla/index.html
index 5572d200..8f654961 100644
--- a/examples/vanilla/index.html
+++ b/examples/vanilla/index.html
@@ -7,6 +7,7 @@
 
     <link data-trunk rel="scss" href="src/index.scss"/>
     <link data-trunk rel="css" href="src/app.css"/>
+    <link data-trunk data-no-minify rel="css" href="src/not_minified.css"/>
     <base data-trunk-public-url/>
 </head>
 <body>
diff --git a/examples/vanilla/src/not_minified.css b/examples/vanilla/src/not_minified.css
new file mode 100644
index 00000000..a3122f19
--- /dev/null
+++ b/examples/vanilla/src/not_minified.css
@@ -0,0 +1,4 @@
+.should_not_be_minified:checked:hover {
+  --empty-prop: ;
+  background-color: black;
+}
diff --git a/site/content/assets.md b/site/content/assets.md
index 202105dd..d557336a 100644
--- a/site/content/assets.md
+++ b/site/content/assets.md
@@ -54,6 +54,7 @@ This will typically look like: `<link data-trunk rel="{type}" href="{path}" ..ot
 
   - In the future, Trunk will resolve local `@imports`, will handle minification (see [trunk#7](https://github.com/trunk-rs/trunk/issues/7)), and we may even look into a pattern where any CSS found in the source tree will be bundled, which would enable a nice zero-config "component styles" pattern. See [trunk#3](https://github.com/trunk-rs/trunk/issues/3) for more details.
   - `data-integrity`: (optional) the `integrity` digest type for code & script resources. Defaults to plain `sha384`.
+  - `data-no-minify`: (optional) by default, CSS files are minified in `--release` mode (unless building with `--no-minification`). Setting this attribute disables minification for that particular file. Defaults to false.
 
 ## tailwind
 
@@ -103,6 +104,8 @@ This will typically look like: `<script data-trunk src="{path}" ..other options
 
 Trunk will copy script files found in the source HTML without content modification. This content is hashed for cache control. The `src` attribute must be included in the script pointing to the script file to be processed.
 
+  - `data-no-minify`: (optional) by default, scripts are minified in `--release` mode (unless building with `--no-minification`). Setting this attribute disables minification for that particular file. Defaults to false.
+
 ## JS Snippets
 
 JS snippets generated from the [wasm-bindgen JS snippets feature](https://rustwasm.github.io/docs/wasm-bindgen/reference/js-snippets.html) are automatically copied to the dist dir, hashed and ready to rock. No additional setup is required. Just use the feature in your application, and Trunk will take care of the rest.
diff --git a/src/pipelines/css.rs b/src/pipelines/css.rs
index b6011f43..41f9e222 100644
--- a/src/pipelines/css.rs
+++ b/src/pipelines/css.rs
@@ -1,6 +1,6 @@
 //! CSS asset pipeline.
 
-use super::{AssetFile, AttrWriter, Attrs, TrunkAssetPipelineOutput, ATTR_HREF};
+use super::{AssetFile, AttrWriter, Attrs, TrunkAssetPipelineOutput, ATTR_HREF, ATTR_MINIFY};
 use crate::{
     config::RtcBuild,
     pipelines::AssetFileType,
@@ -24,6 +24,8 @@ pub struct Css {
     attrs: Attrs,
     /// The required integrity setting
     integrity: IntegrityType,
+    /// Whether to minify or not
+    minify: bool,
 }
 
 impl Css {
@@ -45,12 +47,15 @@ impl Css {
 
         let integrity = IntegrityType::from_attrs(&attrs, &cfg)?;
 
+        let minify = attrs.get(ATTR_MINIFY).is_none();
+
         Ok(Self {
             id,
             cfg,
             asset,
             attrs,
             integrity,
+            minify,
         })
     }
 
@@ -65,12 +70,13 @@ impl Css {
     async fn run(self) -> Result<TrunkAssetPipelineOutput> {
         let rel_path = crate::common::strip_prefix(&self.asset.path);
         tracing::info!(path = ?rel_path, "copying & hashing css");
+        let minify = self.cfg.release && self.minify && !self.cfg.no_minification;
         let file = self
             .asset
             .copy(
                 &self.cfg.staging_dist,
                 self.cfg.filehash,
-                self.cfg.release && !self.cfg.no_minification,
+                minify,
                 AssetFileType::Css,
             )
             .await?;
diff --git a/src/pipelines/js.rs b/src/pipelines/js.rs
index dda2b681..3ae5052e 100644
--- a/src/pipelines/js.rs
+++ b/src/pipelines/js.rs
@@ -1,6 +1,6 @@
 //! JS asset pipeline.
 
-use super::{AssetFile, AttrWriter, Attrs, TrunkAssetPipelineOutput, ATTR_SRC};
+use super::{AssetFile, AttrWriter, Attrs, TrunkAssetPipelineOutput, ATTR_MINIFY, ATTR_SRC};
 use crate::{
     config::RtcBuild,
     pipelines::AssetFileType,
@@ -26,6 +26,8 @@ pub struct Js {
     integrity: IntegrityType,
     /// If it's a JavaScript module (vs a classic script)
     module: bool,
+    /// Whether to minify or not
+    minify: bool,
 }
 
 impl Js {
@@ -44,14 +46,8 @@ impl Js {
         let asset = AssetFile::new(&html_dir, path).await?;
 
         let integrity = IntegrityType::from_attrs(&attrs, &cfg)?;
-
         let module = attrs.get("type").map(|s| s.as_str()) == Some("module");
-
-        // Remove src and data-trunk from attributes.
-        let attrs = attrs
-            .into_iter()
-            .filter(|(x, _)| *x != "src" && !x.starts_with("data-trunk"))
-            .collect();
+        let minify = attrs.get(ATTR_MINIFY).is_none();
 
         Ok(Self {
             id,
@@ -60,6 +56,7 @@ impl Js {
             module,
             attrs,
             integrity,
+            minify,
         })
     }
 
@@ -74,12 +71,13 @@ impl Js {
     async fn run(self) -> Result<TrunkAssetPipelineOutput> {
         let rel_path = crate::common::strip_prefix(&self.asset.path);
         tracing::info!(path = ?rel_path, "copying & hashing js");
+        let minify = self.cfg.release && self.minify && !self.cfg.no_minification;
         let file = self
             .asset
             .copy(
                 &self.cfg.staging_dist,
                 self.cfg.filehash,
-                self.cfg.release && !self.cfg.no_minification,
+                minify,
                 if self.module {
                     AssetFileType::Mjs
                 } else {
@@ -130,7 +128,7 @@ impl JsOutput {
         dom.select(&super::trunk_script_id_selector(self.id))
             .replace_with_html(format!(
                 r#"<script src="{base}{file}"{attrs}/>"#,
-                attrs = AttrWriter::new(&attrs, &[]),
+                attrs = AttrWriter::new(&attrs, AttrWriter::EXCLUDE_SCRIPT),
                 base = &self.cfg.public_url,
                 file = self.file
             ));
diff --git a/src/pipelines/mod.rs b/src/pipelines/mod.rs
index 533e9228..67d06b6d 100644
--- a/src/pipelines/mod.rs
+++ b/src/pipelines/mod.rs
@@ -45,6 +45,7 @@ const ATTR_HREF: &str = "href";
 const ATTR_SRC: &str = "src";
 const ATTR_TYPE: &str = "type";
 const ATTR_REL: &str = "rel";
+const ATTR_MINIFY: &str = "data-no-minify";
 const SNIPPETS_DIR: &str = "snippets";
 const TRUNK_ID: &str = "data-trunk-id";
 const PNG_OPTIMIZATION_LEVEL: u8 = 6;
@@ -352,11 +353,21 @@ impl<'a> AttrWriter<'a> {
         ATTR_INLINE,
         ATTR_SRC,
         ATTR_TYPE,
+        ATTR_MINIFY,
     ];
     /// Whereas on link elements, the MIME type for css is A-OK. You can even specify a custom
     /// MIME type.
-    pub(self) const EXCLUDE_CSS_LINK: &'static [&'static str] =
-        &[TRUNK_ID, ATTR_HREF, ATTR_REL, ATTR_INLINE, ATTR_SRC];
+    pub(self) const EXCLUDE_CSS_LINK: &'static [&'static str] = &[
+        TRUNK_ID,
+        ATTR_HREF,
+        ATTR_REL,
+        ATTR_INLINE,
+        ATTR_SRC,
+        ATTR_MINIFY,
+    ];
+
+    /// Attributes to ignore for <script> tags
+    pub(self) const EXCLUDE_SCRIPT: &'static [&'static str] = &[ATTR_SRC, ATTR_MINIFY];
 
     pub(self) fn new(attrs: &'a Attrs, exclude: &'a [&'a str]) -> Self {
         Self { attrs, exclude }