├── assets ├── asset.txt ├── video.mp4 ├── script.js └── data.json ├── test-package ├── test.mp4 ├── test-package-dependency │ ├── src │ │ ├── asset.txt │ │ ├── lib.rs │ │ └── file.rs │ └── Cargo.toml ├── .DS_Store ├── test-package-nested-dependency │ ├── src │ │ ├── folder.rs │ │ ├── js.rs │ │ ├── lib.rs │ │ ├── font.rs │ │ └── file.rs │ ├── all_the_assets │ │ ├── data.json │ │ ├── style.css │ │ ├── script.js │ │ └── rustacean-flat-gesture.png │ ├── .DS_Store │ └── Cargo.toml ├── Cargo.toml └── src │ └── main.rs ├── .DS_Store ├── common ├── build.rs ├── src │ ├── built.rs │ ├── lib.rs │ ├── manifest.rs │ ├── linker.rs │ ├── cache.rs │ ├── config.rs │ ├── file.rs │ └── asset.rs └── Cargo.toml ├── macro ├── README.md ├── Cargo.toml └── src │ ├── folder.rs │ ├── file.rs │ ├── json.rs │ ├── js.rs │ ├── css.rs │ ├── font.rs │ ├── image.rs │ └── lib.rs ├── .gitignore ├── .github ├── dependabot.yml ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_requst.md │ └── bug_report.md └── workflows │ ├── macos.yml │ ├── windows.yml │ └── main.yml ├── cli-support ├── src │ ├── lib.rs │ ├── marker.rs │ ├── folder.rs │ ├── linker_intercept.rs │ ├── manifest.rs │ └── file.rs ├── README.md ├── examples │ └── cli.rs ├── tests │ └── collects_assets.rs └── Cargo.toml ├── Cargo.toml ├── README.md └── src └── lib.rs /assets/asset.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/video.mp4: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test-package/test.mp4: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test-package/test-package-dependency/src/asset.txt: -------------------------------------------------------------------------------- 1 | This is a test -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DioxusLabs/asset/HEAD/.DS_Store -------------------------------------------------------------------------------- /assets/script.js: -------------------------------------------------------------------------------- 1 | export function hello() { 2 | console.log("Hello world!"); 3 | } -------------------------------------------------------------------------------- /test-package/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DioxusLabs/asset/HEAD/test-package/.DS_Store -------------------------------------------------------------------------------- /assets/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello": "world", 3 | "foo": { 4 | "bar": "baz" 5 | } 6 | } -------------------------------------------------------------------------------- /common/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | built::write_built_file().expect("Failed to acquire build-time information"); 3 | } 4 | -------------------------------------------------------------------------------- /test-package/test-package-nested-dependency/src/folder.rs: -------------------------------------------------------------------------------- 1 | pub const FOLDER: &str = manganis::mg!("./all_the_assets"); 2 | -------------------------------------------------------------------------------- /macro/README.md: -------------------------------------------------------------------------------- 1 | # Manganis Macro 2 | 3 | This crate contains the macro used to interact with the Manganis asset system. 4 | -------------------------------------------------------------------------------- /common/src/built.rs: -------------------------------------------------------------------------------- 1 | // The file `built.rs` was placed there by cargo and `build.rs` 2 | include!(concat!(env!("OUT_DIR"), "/built.rs")); 3 | -------------------------------------------------------------------------------- /test-package/test-package-nested-dependency/all_the_assets/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello": "world", 3 | "foo": { 4 | "bar": "baz" 5 | } 6 | } -------------------------------------------------------------------------------- /test-package/test-package-nested-dependency/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DioxusLabs/asset/HEAD/test-package/test-package-nested-dependency/.DS_Store -------------------------------------------------------------------------------- /test-package/test-package-nested-dependency/all_the_assets/style.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: red; 3 | } 4 | 5 | .bar { 6 | color: red; 7 | width: calc(1px + 1px); 8 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /test-package/dist 3 | /Cargo.lock 4 | /test-package/test-package-nested-dependency/target 5 | /test-package/assets 6 | /cli-support/assets 7 | /dist 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /test-package/test-package-nested-dependency/src/js.rs: -------------------------------------------------------------------------------- 1 | pub const SCRIPT: &str = manganis::mg!(file("./all_the_assets/script.js")); 2 | pub const DATA: &str = manganis::mg!(file("./all_the_assets/data.json")); 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | # Maintain dependencies for GitHub Actions 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | -------------------------------------------------------------------------------- /test-package/test-package-nested-dependency/all_the_assets/script.js: -------------------------------------------------------------------------------- 1 | export function hello() { 2 | console.log("Hello world!"); 3 | local(); 4 | } 5 | function local() { 6 | console.log("minify this") 7 | } -------------------------------------------------------------------------------- /test-package/test-package-nested-dependency/all_the_assets/rustacean-flat-gesture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DioxusLabs/asset/HEAD/test-package/test-package-nested-dependency/all_the_assets/rustacean-flat-gesture.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: DioxusLabs # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | open_collective: dioxus-labs # Replace with a single Open Collective username 5 | -------------------------------------------------------------------------------- /common/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | //! Common types and methods for the manganis asset system 3 | 4 | mod asset; 5 | mod built; 6 | pub mod cache; 7 | mod config; 8 | mod file; 9 | pub mod linker; 10 | mod manifest; 11 | 12 | pub use asset::*; 13 | pub use config::*; 14 | pub use file::*; 15 | pub use manifest::*; 16 | -------------------------------------------------------------------------------- /test-package/test-package-nested-dependency/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-package-nested-dependency" 3 | version = "0.2.1" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | manganis = { path = "../../" } 11 | 12 | -------------------------------------------------------------------------------- /test-package/test-package-dependency/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod file; 2 | pub use file::*; 3 | pub use test_package_nested_dependency::*; 4 | 5 | const _: &str = manganis::classes!("flex flex-col p-1"); 6 | const _: &str = manganis::classes!("flex flex-col p-2"); 7 | const _: &str = manganis::classes!("flex flex-col p-3"); 8 | const _: &str = manganis::classes!("flex flex-col p-4"); 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_requst.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: If you have any interesting advice, you can tell us. 4 | --- 5 | 6 | ## Specific Demand 7 | 8 | 11 | 12 | ## Implement Suggestion 13 | 14 | -------------------------------------------------------------------------------- /test-package/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-package" 3 | version = "0.2.1" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | manganis = { path = "../" } 11 | test-package-dependency = { path = "./test-package-dependency", version = "0.2.1" } 12 | tracing-subscriber = "0.3.17" 13 | -------------------------------------------------------------------------------- /test-package/test-package-dependency/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-package-dependency" 3 | version = "0.2.1" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | manganis = { path = "../../" } 11 | test-package-nested-dependency = { path = "../test-package-nested-dependency" } 12 | -------------------------------------------------------------------------------- /test-package/test-package-dependency/src/file.rs: -------------------------------------------------------------------------------- 1 | pub static FORCE_IMPORT: u32 = 0; 2 | 3 | const _: &str = manganis::classes!("flex flex-col p-5"); 4 | pub const TEXT_ASSET: &str = manganis::mg!("./src/asset.txt"); 5 | pub const IMAGE_ASSET: &str = 6 | manganis::mg!("https://rustacean.net/assets/rustacean-flat-happy.png"); 7 | pub const HTML_ASSET: &str = manganis::mg!("https://github.com/DioxusLabs/dioxus"); 8 | -------------------------------------------------------------------------------- /cli-support/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![deny(missing_docs)] 3 | 4 | #[allow(hidden_glob_reexports)] 5 | mod file; 6 | mod folder; 7 | mod linker_intercept; 8 | mod manifest; 9 | mod marker; 10 | 11 | pub use file::process_file; 12 | pub use folder::process_folder; 13 | pub use linker_intercept::*; 14 | pub use manganis_common::*; 15 | pub use manifest::*; 16 | pub use marker::*; 17 | -------------------------------------------------------------------------------- /test-package/test-package-nested-dependency/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod file; 2 | pub use file::*; 3 | mod font; 4 | pub use font::*; 5 | mod js; 6 | pub use js::*; 7 | mod folder; 8 | pub use folder::*; 9 | 10 | const _: &str = manganis::classes!("flex flex-row p-1"); 11 | const _: &str = manganis::classes!("flex flex-row p-2"); 12 | const _: &str = manganis::classes!("flex flex-row p-3"); 13 | const _: &str = manganis::classes!("flex flex-row p-4"); 14 | -------------------------------------------------------------------------------- /test-package/test-package-nested-dependency/src/font.rs: -------------------------------------------------------------------------------- 1 | pub const ROBOTO_FONT: &str = manganis::mg!(font() 2 | .families(["Roboto"]) 3 | .weights([400]) 4 | .text("hello world")); 5 | pub const COMFORTAA_FONT: &str = manganis::mg!(font() 6 | .families(["Comfortaa"]) 7 | .weights([400]) 8 | .text("hello world")); 9 | pub const ROBOTO_FONT_LIGHT_FONT: &str = manganis::mg!(font() 10 | .families(["Roboto"]) 11 | .weights([200]) 12 | .text("hello world")); 13 | -------------------------------------------------------------------------------- /cli-support/src/marker.rs: -------------------------------------------------------------------------------- 1 | /// This guard tells the marco that the application is being compiled with a CLI that supports assets 2 | /// 3 | /// *If you do not hold this marker when compiling an application that uses the assets macro, the macro will display a warning about asset support* 4 | pub struct ManganisSupportGuard(()); 5 | 6 | impl ManganisSupportGuard { 7 | /// Creates a new marker 8 | pub fn new() -> Self { 9 | Self::default() 10 | } 11 | } 12 | 13 | impl Default for ManganisSupportGuard { 14 | fn default() -> Self { 15 | std::env::set_var("MANGANIS_SUPPORT", "true"); 16 | Self(()) 17 | } 18 | } 19 | 20 | impl Drop for ManganisSupportGuard { 21 | fn drop(&mut self) { 22 | std::env::remove_var("MANGANIS_SUPPORT"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "manganis-common" 3 | version.workspace = true 4 | edition = "2021" 5 | authors = ["Evan Almloff"] 6 | description = "Ergonomic, automatic, cross crate asset collection and optimization" 7 | license = "MIT OR Apache-2.0" 8 | repository = "https://github.com/DioxusLabs/manganis/" 9 | homepage = "https://dioxuslabs.com" 10 | keywords = ["assets"] 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | serde = { version = "1.0.183", features = ["derive"] } 16 | toml = "0.7.6" 17 | anyhow = "1" 18 | home = "0.5.5" 19 | base64 = "0.21.5" 20 | infer = "0.11.0" 21 | 22 | # Remote assets 23 | url = { version = "2.4.0", features = ["serde"] } 24 | tracing = "0.1.40" 25 | 26 | [features] 27 | html = [] 28 | 29 | [build-dependencies] 30 | built = { version = "0.7", features = ["git2"] } 31 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | # Manganese is a rusting catalyst. Manganis makes it faster to collect rust assets (and has almost no google search results) 3 | name = "manganis" 4 | version.workspace = true 5 | authors = ["Evan Almloff"] 6 | edition = "2021" 7 | description = "Ergonomic, automatic, cross crate asset collection and optimization" 8 | license = "MIT OR Apache-2.0" 9 | repository = "https://github.com/DioxusLabs/manganis/" 10 | homepage = "https://dioxuslabs.com" 11 | keywords = ["assets"] 12 | 13 | [lib] 14 | 15 | [dependencies] 16 | manganis-macro = { path = "./macro", version = "0.3.0-alpha.3", optional = true } 17 | 18 | [workspace] 19 | package.version = "0.3.0-alpha.3" 20 | members = ["macro", "common", "cli-support", "test-package", "test-package/test-package-dependency", "test-package/test-package-nested-dependency"] 21 | 22 | [features] 23 | default = ["macro"] 24 | html = [] 25 | url-encoding = ["manganis-macro/url-encoding"] 26 | macro = ["dep:manganis-macro"] 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve Dioxus 4 | --- 5 | 6 | **Problem** 7 | 8 | 9 | 10 | **Steps To Reproduce** 11 | 12 | Steps to reproduce the behavior: 13 | 14 | - 15 | - 16 | - 17 | 18 | **Expected behavior** 19 | 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment:** 27 | - Manganis version: [e.g. v0.17, `main`] 28 | - Rust version: [e.g. 1.43.0, `nightly`] 29 | - OS info: [e.g. MacOS] 30 | - App platform: [e.g. `web`, `desktop`] 31 | 32 | **Questionnaire** 33 | 34 | - [ ] I'm interested in fixing this myself but don't know where to start 35 | - [ ] I would like to fix and I have a solution 36 | - [ ] I don't have time to fix this right now, but maybe later -------------------------------------------------------------------------------- /macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "manganis-macro" 3 | version.workspace = true 4 | edition = "2021" 5 | authors = ["Evan Almloff"] 6 | description = "Ergonomic, automatic, cross crate asset collection and optimization" 7 | license = "MIT OR Apache-2.0" 8 | repository = "https://github.com/DioxusLabs/manganis/" 9 | homepage = "https://dioxuslabs.com" 10 | keywords = ["assets"] 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | proc-macro2 = { version = "1.0" } 17 | quote = "1.0" 18 | syn = { version = "2.0", features = ["full", "extra-traits"] } 19 | manganis-common = { path = "../common", version = "0.3.0-alpha.3" } 20 | manganis-cli-support = { path = "../cli-support", version = "0.3.0-alpha.3", optional = true } 21 | base64 = { version = "0.21.5", optional = true } 22 | tracing-subscriber = "0.3.18" 23 | serde_json = "1.0" 24 | 25 | [build-dependencies] 26 | manganis-common = { path = "../common", version = "0.3.0-alpha.3" } 27 | 28 | [features] 29 | url-encoding = ["manganis-cli-support", "base64"] 30 | -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: macOS tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - cli-support/src/** 9 | - cli-support/examples/** 10 | - cli-support/Cargo.toml 11 | - common/src/** 12 | - common/examples/** 13 | - common/Cargo.toml 14 | - macro/src/** 15 | - macro/examples/** 16 | - macro/Cargo.toml 17 | - examples/** 18 | - src/** 19 | - .github/** 20 | - Cargo.toml 21 | 22 | pull_request: 23 | types: [opened, synchronize, reopened, ready_for_review] 24 | branches: 25 | - main 26 | paths: 27 | - cli-support/src/** 28 | - cli-support/examples/** 29 | - cli-support/Cargo.toml 30 | - common/src/** 31 | - common/examples/** 32 | - common/Cargo.toml 33 | - macro/src/** 34 | - macro/examples/** 35 | - macro/Cargo.toml 36 | - examples/** 37 | - src/** 38 | - .github/** 39 | - Cargo.toml 40 | 41 | jobs: 42 | test: 43 | if: github.event.pull_request.draft == false 44 | name: Test Suite 45 | runs-on: macos-latest 46 | steps: 47 | - uses: dtolnay/rust-toolchain@stable 48 | - uses: Swatinem/rust-cache@v2 49 | - uses: actions/checkout@v4 50 | - run: cargo test --all --tests 51 | -------------------------------------------------------------------------------- /test-package/src/main.rs: -------------------------------------------------------------------------------- 1 | // The assets must be configured with the [CLI](cli-support/examples/cli.rs) before this example can be run. 2 | 3 | use std::path::PathBuf; 4 | 5 | use test_package_dependency::*; 6 | 7 | const TEXT_FILE: &str = manganis::mg!("/test-package-dependency/src/asset.txt"); 8 | const VIDEO_FILE: &str = manganis::mg!("/test.mp4"); 9 | 10 | const ALL_ASSETS: &[&str] = &[ 11 | VIDEO_FILE, 12 | TEXT_FILE, 13 | TEXT_ASSET, 14 | IMAGE_ASSET, 15 | HTML_ASSET, 16 | CSS_ASSET, 17 | PNG_ASSET, 18 | RESIZED_PNG_ASSET.path(), 19 | JPEG_ASSET.path(), 20 | RESIZED_JPEG_ASSET.path(), 21 | AVIF_ASSET.path(), 22 | RESIZED_AVIF_ASSET.path(), 23 | WEBP_ASSET.path(), 24 | RESIZED_WEBP_ASSET.path(), 25 | ROBOTO_FONT, 26 | COMFORTAA_FONT, 27 | ROBOTO_FONT_LIGHT_FONT, 28 | SCRIPT, 29 | DATA, 30 | FOLDER, 31 | ]; 32 | 33 | fn main() { 34 | tracing_subscriber::fmt::init(); 35 | 36 | let external_paths_should_exist: bool = option_env!("MANGANIS_SUPPORT").is_some(); 37 | 38 | // Make sure the macro paths match with the paths that actually exist 39 | for path in ALL_ASSETS { 40 | // Skip remote assets 41 | if path.starts_with("http") { 42 | continue; 43 | } 44 | let path = PathBuf::from(path); 45 | println!("{:?}", path); 46 | assert!(!external_paths_should_exist || path.exists()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test-package/test-package-nested-dependency/src/file.rs: -------------------------------------------------------------------------------- 1 | const _: &str = manganis::classes!("flex flex-row p-4"); 2 | pub const CSS_ASSET: &str = manganis::mg!(file("./all_the_assets/style.css")); 3 | pub const PNG_ASSET: &str = manganis::mg!(file("./all_the_assets/rustacean-flat-gesture.png")); 4 | pub const RESIZED_PNG_ASSET: manganis::ImageAsset = 5 | manganis::mg!(image("./all_the_assets/rustacean-flat-gesture.png").size(52, 52)); 6 | pub const JPEG_ASSET: manganis::ImageAsset = 7 | manganis::mg!(image("./all_the_assets/rustacean-flat-gesture.png").format(ImageType::Jpg)); 8 | pub const RESIZED_JPEG_ASSET: manganis::ImageAsset = 9 | manganis::mg!(image("./all_the_assets/rustacean-flat-gesture.png") 10 | .format(ImageType::Jpg) 11 | .size(52, 52)); 12 | pub const AVIF_ASSET: manganis::ImageAsset = 13 | manganis::mg!(image("./all_the_assets/rustacean-flat-gesture.png").format(ImageType::Avif)); 14 | pub const RESIZED_AVIF_ASSET: manganis::ImageAsset = 15 | manganis::mg!(image("./all_the_assets/rustacean-flat-gesture.png") 16 | .format(ImageType::Avif) 17 | .size(52, 52)); 18 | pub const WEBP_ASSET: manganis::ImageAsset = 19 | manganis::mg!(image("./all_the_assets/rustacean-flat-gesture.png").format(ImageType::Webp)); 20 | pub const RESIZED_WEBP_ASSET: manganis::ImageAsset = 21 | manganis::mg!(image("./all_the_assets/rustacean-flat-gesture.png") 22 | .format(ImageType::Webp) 23 | .size(52, 52)); 24 | -------------------------------------------------------------------------------- /macro/src/folder.rs: -------------------------------------------------------------------------------- 1 | use manganis_common::{AssetSource, AssetType, FolderAsset, ManganisSupportError}; 2 | use quote::{quote, ToTokens}; 3 | use syn::{parenthesized, parse::Parse}; 4 | 5 | use crate::generate_link_section; 6 | 7 | pub struct FolderAssetParser { 8 | file_name: Result, 9 | asset: AssetType, 10 | } 11 | 12 | impl Parse for FolderAssetParser { 13 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 14 | let inside; 15 | parenthesized!(inside in input); 16 | let path = inside.parse::()?; 17 | 18 | let path_as_str = path.value(); 19 | let path = match AssetSource::parse_folder(&path_as_str) { 20 | Ok(path) => path, 21 | Err(e) => return Err(syn::Error::new(proc_macro2::Span::call_site(), e)), 22 | }; 23 | let this_file = FolderAsset::new(path); 24 | let asset = manganis_common::AssetType::Folder(this_file.clone()); 25 | 26 | let file_name = this_file.served_location(); 27 | 28 | Ok(FolderAssetParser { file_name, asset }) 29 | } 30 | } 31 | 32 | impl ToTokens for FolderAssetParser { 33 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 34 | let file_name = crate::quote_path(&self.file_name); 35 | 36 | let link_section = generate_link_section(self.asset.clone()); 37 | 38 | tokens.extend(quote! { 39 | { 40 | #link_section 41 | #file_name 42 | } 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /macro/src/file.rs: -------------------------------------------------------------------------------- 1 | use manganis_common::{AssetSource, AssetType, FileAsset, ManganisSupportError}; 2 | use quote::{quote, ToTokens}; 3 | use syn::{parenthesized, parse::Parse}; 4 | 5 | use crate::generate_link_section; 6 | 7 | pub struct FileAssetParser { 8 | file_name: Result, 9 | asset: AssetType, 10 | } 11 | 12 | impl Parse for FileAssetParser { 13 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 14 | let inside; 15 | parenthesized!(inside in input); 16 | let path = inside.parse::()?; 17 | 18 | let path_as_str = path.value(); 19 | let path = match AssetSource::parse_file(&path_as_str) { 20 | Ok(path) => path, 21 | Err(e) => { 22 | return Err(syn::Error::new( 23 | proc_macro2::Span::call_site(), 24 | format!("{e}"), 25 | )) 26 | } 27 | }; 28 | let this_file = FileAsset::new(path); 29 | let asset = manganis_common::AssetType::File(this_file.clone()); 30 | 31 | let file_name = this_file.served_location(); 32 | 33 | Ok(FileAssetParser { file_name, asset }) 34 | } 35 | } 36 | 37 | impl ToTokens for FileAssetParser { 38 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 39 | let file_name = crate::quote_path(&self.file_name); 40 | 41 | let link_section = generate_link_section(self.asset.clone()); 42 | 43 | tokens.extend(quote! { 44 | { 45 | #link_section 46 | #file_name 47 | } 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cli-support/src/folder.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use manganis_common::{FileOptions, FolderAsset}; 4 | use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; 5 | 6 | use crate::file::Process; 7 | 8 | /// Process a folder, optimizing and copying all assets into the output folder 9 | pub fn process_folder(folder: &FolderAsset, output_folder: &Path) -> anyhow::Result<()> { 10 | // Push the unique name of the folder to the output folder 11 | let output_folder = output_folder.join(folder.unique_name()); 12 | 13 | if output_folder.exists() { 14 | return Ok(()); 15 | } 16 | 17 | let folder = folder 18 | .location() 19 | .source() 20 | .as_path() 21 | .expect("Folder asset must be a local path"); 22 | 23 | // Optimize and copy all assets in the folder in parallel 24 | process_folder_inner(folder, &output_folder) 25 | } 26 | 27 | fn process_folder_inner(folder: &Path, output_folder: &Path) -> anyhow::Result<()> { 28 | // Create the folder 29 | std::fs::create_dir_all(output_folder)?; 30 | 31 | // Then optimize children 32 | let files: Vec<_> = std::fs::read_dir(folder) 33 | .into_iter() 34 | .flatten() 35 | .flatten() 36 | .collect(); 37 | 38 | files.par_iter().try_for_each(|file| { 39 | let file = file.path(); 40 | let metadata = file.metadata()?; 41 | let output_path = output_folder.join(file.strip_prefix(folder)?); 42 | if metadata.is_dir() { 43 | process_folder_inner(&file, &output_path) 44 | } else { 45 | process_file_minimal(&file, &output_path) 46 | } 47 | })?; 48 | 49 | Ok(()) 50 | } 51 | 52 | /// Optimize a file without changing any of its contents significantly (e.g. by changing the extension) 53 | fn process_file_minimal(input_path: &Path, output_path: &Path) -> anyhow::Result<()> { 54 | let options = 55 | FileOptions::default_for_extension(input_path.extension().and_then(|e| e.to_str())); 56 | let source = manganis_common::AssetSource::Local(input_path.to_path_buf()); 57 | options.process(&source, output_path)?; 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /cli-support/README.md: -------------------------------------------------------------------------------- 1 | # Manganis CLI Support 2 | 3 | This crate provides utilities to collect assets that integrate with the Manganis macro. It makes it easy to integrate an asset collection and optimization system into a build tool. 4 | 5 | ```rust, no_run 6 | use manganis_cli_support::{AssetManifestExt, ManganisSupportGuard}; 7 | use manganis_common::{AssetManifest, Config}; 8 | use std::process::Command; 9 | 10 | // This is the location where the assets will be copied to in the filesystem 11 | let assets_file_location = "./assets"; 12 | // This is the location where the assets will be served from 13 | let assets_serve_location = "/assets"; 14 | 15 | // First set any settings you need for the build. 16 | Config::default() 17 | .with_assets_serve_location(assets_serve_location) 18 | .save(); 19 | 20 | // Tell manganis that you support assets 21 | let _guard = ManganisSupportGuard::default(); 22 | 23 | // Determine if Rust is trying to link: 24 | if let Some((_working_dir, object_files)) = manganis_cli_support::linker_intercept(std::env::args()) { 25 | // If it is, collect the assets. 26 | let manifest = AssetManifest::load(object_files); 27 | 28 | // Remove the old assets 29 | let _ = std::fs::remove_dir_all(assets_file_location); 30 | 31 | // And copy the static assets to the public directory 32 | manifest 33 | .copy_static_assets_to(assets_file_location) 34 | .unwrap(); 35 | 36 | // Then collect the tailwind CSS 37 | let css = manifest.collect_tailwind_css(true, &mut Vec::new()); 38 | 39 | // And write the CSS to the public directory 40 | std::fs::write(format!("{}/tailwind.css", assets_file_location), css).unwrap(); 41 | 42 | } else { 43 | // If it isn't, build your app and initiate the helper function `start_linker_intercept()` 44 | 45 | // Put any cargo args in a slice that should also be passed 46 | // to manganis toreproduce the same build. e.g. the `--release` flag 47 | let args: Vec<&str> = vec![]; 48 | Command::new("cargo") 49 | .arg("build") 50 | .args(args.clone()) 51 | .spawn() 52 | .unwrap() 53 | .wait() 54 | .unwrap(); 55 | 56 | manganis_cli_support::start_linker_intercept(None, args).unwrap(); 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /common/src/manifest.rs: -------------------------------------------------------------------------------- 1 | use crate::AssetType; 2 | 3 | /// A manifest of all assets collected from dependencies 4 | #[derive(Debug, PartialEq, Default, Clone)] 5 | pub struct AssetManifest { 6 | pub(crate) assets: Vec, 7 | } 8 | 9 | impl AssetManifest { 10 | /// Creates a new asset manifest 11 | pub fn new(assets: Vec) -> Self { 12 | Self { assets } 13 | } 14 | 15 | /// Returns all assets collected from dependencies 16 | pub fn assets(&self) -> &Vec { 17 | &self.assets 18 | } 19 | 20 | #[cfg(feature = "html")] 21 | /// Returns the HTML that should be injected into the head of the page 22 | pub fn head(&self) -> String { 23 | let mut head = String::new(); 24 | for asset in &self.assets { 25 | if let crate::AssetType::File(file) = asset { 26 | match file.options() { 27 | crate::FileOptions::Css(css_options) => { 28 | if css_options.preload() { 29 | if let Ok(asset_path) = file.served_location() { 30 | head.push_str(&format!( 31 | "\n" 32 | )) 33 | } 34 | } 35 | } 36 | crate::FileOptions::Image(image_options) => { 37 | if image_options.preload() { 38 | if let Ok(asset_path) = file.served_location() { 39 | head.push_str(&format!( 40 | "\n" 41 | )) 42 | } 43 | } 44 | } 45 | crate::FileOptions::Js(js_options) => { 46 | if js_options.preload() { 47 | if let Ok(asset_path) = file.served_location() { 48 | head.push_str(&format!( 49 | "\n" 50 | )) 51 | } 52 | } 53 | } 54 | _ => {} 55 | } 56 | } 57 | } 58 | head 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /common/src/linker.rs: -------------------------------------------------------------------------------- 1 | // code taken from https://github.com/dtolnay/linkme/, 2 | // MIT license 3 | 4 | //! name conventions used by the linker on different platforms. 5 | //! This is used to make the "link_section" magic working 6 | 7 | /// Information about the manganis link section for a given platform 8 | #[derive(Debug, Clone, Copy)] 9 | pub struct LinkSection { 10 | /// The link section we pass to the static 11 | pub link_section: &'static str, 12 | /// The name of the section we find in the binary 13 | pub name: &'static str, 14 | } 15 | 16 | impl LinkSection { 17 | /// The list of link sections for all supported platforms 18 | pub const ALL: &'static [&'static LinkSection] = 19 | &[Self::WASM, Self::MACOS, Self::WINDOWS, Self::ILLUMOS]; 20 | 21 | /// Returns the link section used in linux, android, fuchsia, psp, freebsd, and wasm32 22 | pub const WASM: &'static LinkSection = &LinkSection { 23 | link_section: "manganis", 24 | name: "manganis", 25 | }; 26 | 27 | /// Returns the link section used in macOS, iOS, tvOS 28 | pub const MACOS: &'static LinkSection = &LinkSection { 29 | link_section: "__DATA,manganis,regular,no_dead_strip", 30 | name: "manganis", 31 | }; 32 | 33 | /// Returns the link section used in windows 34 | pub const WINDOWS: &'static LinkSection = &LinkSection { 35 | link_section: "mg", 36 | name: "mg", 37 | }; 38 | 39 | /// Returns the link section used in illumos 40 | pub const ILLUMOS: &'static LinkSection = &LinkSection { 41 | link_section: "set_manganis", 42 | name: "set_manganis", 43 | }; 44 | 45 | /// The link section used on the current platform 46 | pub const CURRENT: &'static LinkSection = { 47 | #[cfg(any( 48 | target_os = "none", 49 | target_os = "linux", 50 | target_os = "android", 51 | target_os = "fuchsia", 52 | target_os = "psp", 53 | target_os = "freebsd", 54 | target_arch = "wasm32" 55 | ))] 56 | { 57 | Self::WASM 58 | } 59 | 60 | #[cfg(any(target_os = "macos", target_os = "ios", target_os = "tvos"))] 61 | { 62 | Self::MACOS 63 | } 64 | 65 | #[cfg(target_os = "windows")] 66 | { 67 | Self::WINDOWS 68 | } 69 | 70 | #[cfg(target_os = "illumos")] 71 | { 72 | Self::ILLUMOS 73 | } 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /common/src/cache.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for the cache that is used to collect assets 2 | 3 | use std::{ 4 | fmt::{Display, Write}, 5 | path::PathBuf, 6 | }; 7 | 8 | use home::cargo_home; 9 | 10 | /// The location where assets are cached 11 | pub fn asset_cache_dir() -> PathBuf { 12 | let mut dir = cargo_home().unwrap(); 13 | dir.push("assets"); 14 | dir 15 | } 16 | 17 | pub(crate) fn config_path() -> PathBuf { 18 | asset_cache_dir().join("config.toml") 19 | } 20 | 21 | pub(crate) fn current_package_identifier() -> String { 22 | package_identifier( 23 | &std::env::var("CARGO_PKG_NAME").unwrap(), 24 | std::env::var("CARGO_BIN_NAME").ok().as_deref(), 25 | current_package_version(), 26 | ) 27 | } 28 | 29 | /// The identifier for a package used to cache assets 30 | pub fn package_identifier(package: &str, bin: Option<&str>, version: impl Display) -> String { 31 | let mut id = String::new(); 32 | push_package_identifier(package, bin, version, &mut id); 33 | id 34 | } 35 | 36 | #[deprecated(since = "0.2.3", note = "Use `push_package_identifier` instead")] 37 | /// Like `package_identifier`, but appends the identifier to the given writer 38 | pub fn push_package_cache_dir( 39 | package: &str, 40 | bin: Option<&str>, 41 | version: impl Display, 42 | to: &mut impl Write, 43 | ) { 44 | push_package_identifier(package, bin, version, to); 45 | } 46 | 47 | /// Like `package_identifier`, but appends the identifier to the given writer 48 | pub fn push_package_identifier( 49 | package: &str, 50 | bin: Option<&str>, 51 | version: impl Display, 52 | to: &mut impl Write, 53 | ) { 54 | to.write_str(package).unwrap(); 55 | if let Some(bin) = bin { 56 | to.write_char('-').unwrap(); 57 | to.write_str(bin).unwrap(); 58 | } 59 | to.write_char('-').unwrap(); 60 | to.write_fmt(format_args!("{}", version)).unwrap(); 61 | } 62 | 63 | pub(crate) fn current_package_version() -> String { 64 | std::env::var("CARGO_PKG_VERSION").unwrap() 65 | } 66 | 67 | pub(crate) fn manifest_dir() -> PathBuf { 68 | std::env::var("CARGO_MANIFEST_DIR").unwrap().into() 69 | } 70 | 71 | /// The location where logs are stored while expanding macros 72 | pub fn macro_log_directory() -> PathBuf { 73 | let mut dir = asset_cache_dir(); 74 | dir.push("logs"); 75 | dir 76 | } 77 | 78 | /// The current log file for the macro expansion 79 | pub fn macro_log_file() -> PathBuf { 80 | let mut dir = macro_log_directory(); 81 | dir.push(current_package_identifier()); 82 | dir 83 | } 84 | -------------------------------------------------------------------------------- /common/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::cache::config_path; 6 | 7 | fn default_assets_serve_location() -> String { 8 | #[cfg(target_arch = "wasm32")] 9 | { 10 | "/".to_string() 11 | } 12 | #[cfg(not(target_arch = "wasm32"))] 13 | { 14 | "./assets/".to_string() 15 | } 16 | } 17 | 18 | /// The configuration for collecting assets 19 | #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] 20 | pub struct Config { 21 | #[serde(default = "default_assets_serve_location")] 22 | assets_serve_location: String, 23 | } 24 | 25 | impl Config { 26 | /// The location where assets will be served from. On web applications, this should be the URL path to the directory where assets are served from. 27 | pub fn with_assets_serve_location(&self, assets_serve_location: impl Into) -> Self { 28 | Self { 29 | assets_serve_location: assets_serve_location.into(), 30 | } 31 | } 32 | 33 | /// The location where assets will be served from. On web applications, this should be the URL path to the directory where assets are served from. 34 | pub fn assets_serve_location(&self) -> &str { 35 | &self.assets_serve_location 36 | } 37 | 38 | #[doc(hidden)] 39 | /// Returns the path to the config 40 | /// This is only used in the macro 41 | pub fn config_path() -> PathBuf { 42 | config_path() 43 | } 44 | 45 | /// Returns the current config 46 | pub fn current() -> Self { 47 | std::fs::read(config_path()) 48 | .ok() 49 | .and_then(|config| toml::from_str(&String::from_utf8_lossy(&config)).ok()) 50 | .unwrap_or_default() 51 | } 52 | 53 | /// Saves the config globally. This must be run before compiling the application you are collecting assets from. 54 | /// 55 | /// The assets macro will read the config from the global config file and set the assets serve location to the value in the config. 56 | pub fn save(&self) { 57 | let current = Self::current(); 58 | if current == *self { 59 | return; 60 | } 61 | 62 | let config = toml::to_string(&self).unwrap(); 63 | let config_path = config_path(); 64 | std::fs::create_dir_all(config_path.parent().unwrap()).unwrap(); 65 | std::fs::write(config_path, config).unwrap(); 66 | } 67 | } 68 | 69 | impl Default for Config { 70 | fn default() -> Self { 71 | Self { 72 | assets_serve_location: default_assets_serve_location(), 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /cli-support/examples/cli.rs: -------------------------------------------------------------------------------- 1 | use manganis_cli_support::{AssetManifestExt, ManganisSupportGuard}; 2 | use manganis_common::{AssetManifest, Config}; 3 | use std::{path::PathBuf, process::Command}; 4 | 5 | // This is the location where the assets will be copied to in the filesystem 6 | const ASSETS_FILE_LOCATION: &str = "./assets"; 7 | 8 | // This is the location where the assets will be served from 9 | const ASSETS_SERVE_LOCATION: &str = "./assets/"; 10 | 11 | fn main() { 12 | tracing_subscriber::fmt::init(); 13 | 14 | // First set any settings you need for the build. 15 | Config::default() 16 | .with_assets_serve_location(ASSETS_SERVE_LOCATION) 17 | .save(); 18 | 19 | // Next, tell manganis that you support assets 20 | let _guard = ManganisSupportGuard::default(); 21 | 22 | // Handle the commands. 23 | let args: Vec = std::env::args().collect(); 24 | if let Some(arg) = args.get(1) { 25 | if arg == "link" { 26 | link(); 27 | return; 28 | } else if arg == "build" { 29 | println!("Building!"); 30 | build(); 31 | return; 32 | } 33 | } 34 | 35 | println!("Unknown Command"); 36 | } 37 | 38 | fn build() { 39 | // Build your application 40 | let current_dir = std::env::current_dir().unwrap(); 41 | 42 | let args = ["--release"]; 43 | Command::new("cargo") 44 | .current_dir(¤t_dir) 45 | .arg("build") 46 | .args(args) 47 | .spawn() 48 | .unwrap() 49 | .wait() 50 | .unwrap(); 51 | 52 | // Call the helper function to intercept the Rust linker. 53 | // We will pass the current working directory as it may get lost. 54 | let work_dir = std::env::current_dir().unwrap(); 55 | let link_args = vec![format!("{}", work_dir.display())]; 56 | manganis_cli_support::start_linker_intercept("link", args, Some(link_args)).unwrap(); 57 | } 58 | 59 | fn link() { 60 | let (link_args, object_files) = 61 | manganis_cli_support::linker_intercept(std::env::args()).unwrap(); 62 | 63 | // Extract the assets 64 | let assets = AssetManifest::load_from_objects(object_files); 65 | 66 | let working_dir = PathBuf::from(link_args.first().unwrap()); 67 | let assets_dir = working_dir.join(working_dir.join(ASSETS_FILE_LOCATION)); 68 | 69 | // Remove the old assets 70 | let _ = std::fs::remove_dir_all(&assets_dir); 71 | 72 | // And copy the static assets to the public directory 73 | assets.copy_static_assets_to(&assets_dir).unwrap(); 74 | 75 | // Then collect the tailwind CSS 76 | let css = assets.collect_tailwind_css(true, &mut Vec::new()); 77 | 78 | // And write the CSS to the public directory 79 | let tailwind_path = assets_dir.join("tailwind.css"); 80 | std::fs::write(tailwind_path, css).unwrap(); 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Manganis 2 | 3 | # ⚠️ **Manganis has moved into the main diouxs repo [here](https://github.com/DioxusLabs/dioxus/tree/main/packages/manganis)** 4 | 5 | The Manganis allows you to submit assets to a build tool that supports collecting assets. It makes it easy to self-host assets that are distributed throughout your library. Manganis also handles optimizing, converting, and fetching assets. 6 | 7 | If you defined this in a component library: 8 | ```rust 9 | const AVIF_ASSET: &str = manganis::mg!("rustacean-flat-gesture.png"); 10 | ``` 11 | 12 | AVIF_ASSET will be set to a new file name that will be served by some CLI. That file can be collected by any package that depends on the component library. 13 | 14 | ```rust 15 | // You can include tailwind classes that will be collected into the final binary 16 | const TAILWIND_CLASSES: &str = manganis::classes!("flex flex-col p-5"); 17 | 18 | // You can also collect arbitrary files. Relative paths are resolved relative to the package root 19 | const _: &str = manganis::mg!("test-package-dependency/src/asset.txt"); 20 | // You can use URLs to copy read the asset at build time 21 | const _: &str = manganis::mg!("https://rustacean.net/assets/rustacean-flat-happy.png"); 22 | 23 | // You can collect images which will be automatically optimized 24 | pub const PNG_ASSET: manganis::ImageAsset = 25 | manganis::mg!(image("rustacean-flat-gesture.png")); 26 | // Resize the image at compile time to make the assets smaller 27 | pub const RESIZED_PNG_ASSET: manganis::ImageAsset = 28 | manganis::mg!(image("rustacean-flat-gesture.png").size(52, 52)); 29 | // Or convert the image at compile time to a web friendly format 30 | pub const AVIF_ASSET: manganis::ImageAsset = manganis::mg!(image("rustacean-flat-gesture.png") 31 | .format(ImageType::Avif)); 32 | // You can even include a low quality preview of the image embedded into the url 33 | pub const AVIF_ASSET_LOW: manganis::ImageAsset = manganis::mg!(image("rustacean-flat-gesture.png") 34 | .format(ImageType::Avif) 35 | .low_quality_preview()); 36 | 37 | // You can also collect google fonts 38 | pub const ROBOTO_FONT: &str = manganis::mg!(font() 39 | .families(["Roboto"])); 40 | // Specify weights for fonts to collect 41 | pub const COMFORTAA_FONT: &str = manganis::mg!(font() 42 | .families(["Comfortaa"]) 43 | .weights([400])); 44 | // Or specific text to include only the characters used in that text 45 | pub const ROBOTO_FONT_LIGHT_FONT: &str = manganis::mg!(font() 46 | .families(["Roboto"]) 47 | .weights([200]) 48 | .text("hello world")); 49 | ``` 50 | 51 | ## Adding Support to Your CLI 52 | 53 | To add support for your CLI, you need to integrate with the [manganis_cli_support](https://github.com/DioxusLabs/manganis/tree/main/cli-support) crate. This crate provides utilities to collect assets that integrate with the Manganis macro. It makes it easy to integrate an asset collection and optimization system into a build tool. 54 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: windows 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - cli-support/src/** 9 | - cli-support/examples/** 10 | - cli-support/Cargo.toml 11 | - common/src/** 12 | - common/examples/** 13 | - common/Cargo.toml 14 | - macro/src/** 15 | - macro/examples/** 16 | - macro/Cargo.toml 17 | - examples/** 18 | - src/** 19 | - .github/** 20 | - Cargo.toml 21 | 22 | pull_request: 23 | types: [opened, synchronize, reopened, ready_for_review] 24 | branches: 25 | - main 26 | paths: 27 | - cli-support/src/** 28 | - cli-support/examples/** 29 | - cli-support/Cargo.toml 30 | - common/src/** 31 | - common/examples/** 32 | - common/Cargo.toml 33 | - macro/src/** 34 | - macro/examples/** 35 | - macro/Cargo.toml 36 | - examples/** 37 | - src/** 38 | - .github/** 39 | - Cargo.toml 40 | 41 | jobs: 42 | test: 43 | if: github.event.pull_request.draft == false 44 | runs-on: windows-latest 45 | name: (${{ matrix.target }}, ${{ matrix.cfg_release_channel }}) 46 | env: 47 | CFG_RELEASE_CHANNEL: ${{ matrix.cfg_release_channel }} 48 | strategy: 49 | # https://help.github.com/en/actions/getting-started-with-github-actions/about-github-actions#usage-limits 50 | # There's a limit of 60 concurrent jobs across all repos in the rust-lang organization. 51 | # In order to prevent overusing too much of that 60 limit, we throttle the 52 | # number of rustfmt jobs that will run concurrently. 53 | # max-parallel: 54 | # fail-fast: false 55 | matrix: 56 | target: [x86_64-pc-windows-gnu, x86_64-pc-windows-msvc] 57 | cfg_release_channel: [stable] 58 | 59 | steps: 60 | # The Windows runners have autocrlf enabled by default 61 | # which causes failures for some of rustfmt's line-ending sensitive tests 62 | - name: disable git eol translation 63 | run: git config --global core.autocrlf false 64 | 65 | # Run build 66 | - name: Install Rustup using win.rustup.rs 67 | run: | 68 | # Disable the download progress bar which can cause perf issues 69 | $ProgressPreference = "SilentlyContinue" 70 | Invoke-WebRequest https://win.rustup.rs/ -OutFile rustup-init.exe 71 | .\rustup-init.exe -y --default-host=x86_64-pc-windows-msvc --default-toolchain=none 72 | del rustup-init.exe 73 | rustup target add ${{ matrix.target }} 74 | shell: powershell 75 | 76 | - name: Add mingw64 to path for x86_64-gnu 77 | run: echo "C:\msys64\mingw64\bin" >> $GITHUB_PATH 78 | if: matrix.target == 'x86_64-pc-windows-gnu' && matrix.channel == 'nightly' 79 | shell: bash 80 | 81 | # - name: checkout 82 | # uses: actions/checkout@v3 83 | # with: 84 | # path: C:/manganis.git 85 | # fetch-depth: 1 86 | 87 | # we need to use the C drive as the working directory 88 | 89 | - name: Checkout 90 | run: | 91 | mkdir C:/manganis.git 92 | git clone https://github.com/dioxuslabs/manganis.git C:/manganis.git --depth 1 93 | 94 | - name: test 95 | working-directory: C:/manganis.git 96 | run: | 97 | rustc -Vv 98 | cargo -V 99 | set RUST_BACKTRACE=1 100 | cargo build --all --tests --examples 101 | cargo test --all --tests 102 | shell: cmd 103 | -------------------------------------------------------------------------------- /cli-support/tests/collects_assets.rs: -------------------------------------------------------------------------------- 1 | use manganis_cli_support::{AssetManifestExt, ManganisSupportGuard}; 2 | use manganis_common::{AssetManifest, AssetType, Config}; 3 | use std::path::PathBuf; 4 | use std::process::{Command, Stdio}; 5 | 6 | #[test] 7 | fn collects_assets() { 8 | tracing_subscriber::fmt::init(); 9 | 10 | // This is the location where the assets will be served from 11 | let assets_serve_location = "/"; 12 | 13 | // First set any settings you need for the build 14 | Config::default() 15 | .with_assets_serve_location(assets_serve_location) 16 | .save(); 17 | 18 | // Next, tell manganis that you support assets 19 | let _guard = ManganisSupportGuard::default(); 20 | 21 | // Get args and default to "build" 22 | let args: Vec = std::env::args().collect(); 23 | let command = match args.get(1) { 24 | Some(a) => a.clone(), 25 | None => "build".to_string(), 26 | }; 27 | 28 | // Check if rustc is trying to link 29 | if command == "link" { 30 | link(); 31 | } else { 32 | build(); 33 | } 34 | } 35 | 36 | fn build() { 37 | // Find the test package directory which is up one directory from this package 38 | let mut test_package_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) 39 | .parent() 40 | .unwrap() 41 | .to_path_buf(); 42 | test_package_dir.push("test-package"); 43 | 44 | println!("running the CLI from {test_package_dir:?}"); 45 | 46 | // Then build your application 47 | let args = ["--target", "wasm32-unknown-unknown", "--release"]; 48 | Command::new("cargo") 49 | .arg("build") 50 | .args(args) 51 | .current_dir(&test_package_dir) 52 | .stdout(Stdio::piped()) 53 | .spawn() 54 | .unwrap() 55 | .wait() 56 | .unwrap(); 57 | 58 | println!("Collecting Assets"); 59 | 60 | // Call the helper function to intercept the Rust linker. 61 | // We will pass the current working directory as it may get lost. 62 | let link_args = vec![format!("{}", test_package_dir.display())]; 63 | manganis_cli_support::start_linker_intercept("link", args, Some(link_args)).unwrap(); 64 | } 65 | 66 | fn link() { 67 | let (link_args, object_files) = 68 | manganis_cli_support::linker_intercept(std::env::args()).unwrap(); 69 | 70 | // Recover the working directory from the link args. 71 | let working_dir = PathBuf::from(link_args.first().unwrap()); 72 | 73 | // Then collect the assets 74 | let assets = AssetManifest::load_from_objects(object_files); 75 | 76 | let all_assets = assets.assets(); 77 | println!("{:#?}", all_assets); 78 | 79 | let locations = all_assets 80 | .iter() 81 | .filter_map(|a| match a { 82 | AssetType::File(f) => Some(f.location()), 83 | _ => None, 84 | }) 85 | .collect::>(); 86 | 87 | // Make sure the right number of assets were collected 88 | assert_eq!(locations.len(), 16); 89 | 90 | // Then copy the assets to a temporary directory and run the application 91 | let assets_dir = PathBuf::from("./assets"); 92 | assets.copy_static_assets_to(assets_dir).unwrap(); 93 | 94 | // Then run the application 95 | let status = Command::new("cargo") 96 | .arg("run") 97 | .arg("--release") 98 | .current_dir(&working_dir) 99 | .status() 100 | .unwrap(); 101 | 102 | // Make sure the application exited successfully 103 | assert!(status.success()); 104 | } 105 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Rust CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - cli-support/src/** 9 | - cli-support/examples/** 10 | - cli-support/Cargo.toml 11 | - common/src/** 12 | - common/examples/** 13 | - common/Cargo.toml 14 | - macro/src/** 15 | - macro/examples/** 16 | - macro/Cargo.toml 17 | - examples/** 18 | - docs/guide/** 19 | - src/** 20 | - .github/** 21 | - Cargo.toml 22 | 23 | pull_request: 24 | types: [opened, synchronize, reopened, ready_for_review] 25 | branches: 26 | - main 27 | paths: 28 | - cli-support/src/** 29 | - cli-support/examples/** 30 | - cli-support/Cargo.toml 31 | - common/src/** 32 | - common/examples/** 33 | - common/Cargo.toml 34 | - macro/src/** 35 | - macro/examples/** 36 | - macro/Cargo.toml 37 | - examples/** 38 | - docs/guide/** 39 | - src/** 40 | - .github/** 41 | - Cargo.toml 42 | 43 | env: 44 | CARGO_TERM_COLOR: always 45 | CARGO_INCREMENTAL: 0 46 | # RUSTFLAGS: -Dwarnings 47 | RUST_BACKTRACE: 1 48 | RUSTUP_WINDOWS_PATH_ADD_BIN: 1 49 | # Change to specific Rust release to pin 50 | rust_stable: stable 51 | rust_nightly: nightly-2024-07-01 52 | rust_clippy: "1.79" 53 | # When updating this, also update relevant msrvs (readme, cargo.toml etc): 54 | rust_min: "1.79.0" 55 | 56 | jobs: 57 | check: 58 | if: github.event.pull_request.draft == false 59 | name: Check 60 | runs-on: ubuntu-latest 61 | steps: 62 | - uses: dtolnay/rust-toolchain@stable 63 | - uses: Swatinem/rust-cache@v2 64 | - run: sudo apt-get update 65 | - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev 66 | - uses: actions/checkout@v4 67 | - run: cargo check --all --examples --tests 68 | 69 | test: 70 | if: github.event.pull_request.draft == false 71 | name: Test Suite 72 | runs-on: ubuntu-latest 73 | steps: 74 | - uses: dtolnay/rust-toolchain@stable 75 | - uses: Swatinem/rust-cache@v2 76 | - run: sudo apt-get update 77 | - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev 78 | - uses: actions/checkout@v4 79 | - run: cargo test --lib --bins --tests --examples --workspace 80 | 81 | fmt: 82 | if: github.event.pull_request.draft == false 83 | name: Rustfmt 84 | runs-on: ubuntu-latest 85 | steps: 86 | - uses: dtolnay/rust-toolchain@stable 87 | - uses: Swatinem/rust-cache@v2 88 | - run: rustup component add rustfmt 89 | - uses: actions/checkout@v4 90 | - run: cargo fmt --all -- --check 91 | 92 | clippy: 93 | if: github.event.pull_request.draft == false 94 | name: Clippy 95 | runs-on: ubuntu-latest 96 | steps: 97 | - uses: dtolnay/rust-toolchain@stable 98 | - uses: Swatinem/rust-cache@v2 99 | - run: sudo apt-get update 100 | - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev 101 | - run: rustup component add clippy 102 | - uses: actions/checkout@v4 103 | - run: cargo clippy --workspace --examples --tests -- -D warnings 104 | 105 | docs: 106 | if: github.event.pull_request.draft == false 107 | name: Docs 108 | runs-on: ubuntu-latest 109 | steps: 110 | - uses: actions/checkout@v4 111 | - name: Install Rust ${{ env.rust_nightly }} 112 | uses: dtolnay/rust-toolchain@stable 113 | with: 114 | toolchain: ${{ env.rust_nightly }} 115 | - uses: Swatinem/rust-cache@v2 116 | with: 117 | cache-all-crates: "true" 118 | save-if: ${{ github.ref == 'refs/heads/main' }} 119 | - run: sudo apt-get update 120 | - run: sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev nasm 121 | - name: "doc --lib --all-features" 122 | run: | 123 | cargo doc --workspace --lib --no-deps --all-features --document-private-items 124 | env: 125 | RUSTFLAGS: --cfg docsrs 126 | RUSTDOCFLAGS: --cfg docsrs -Dwarnings 127 | -------------------------------------------------------------------------------- /macro/src/json.rs: -------------------------------------------------------------------------------- 1 | use manganis_common::{AssetSource, AssetType, FileAsset, FileOptions, ManganisSupportError}; 2 | use quote::{quote, ToTokens}; 3 | use syn::{parenthesized, parse::Parse}; 4 | 5 | use crate::generate_link_section; 6 | 7 | struct ParseJsonOptions { 8 | options: Vec, 9 | } 10 | 11 | impl ParseJsonOptions { 12 | fn apply_to_options(self, file: &mut FileAsset) { 13 | for option in self.options { 14 | option.apply_to_options(file); 15 | } 16 | } 17 | } 18 | 19 | impl Parse for ParseJsonOptions { 20 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 21 | let mut options = Vec::new(); 22 | while !input.is_empty() { 23 | options.push(input.parse::()?); 24 | } 25 | Ok(ParseJsonOptions { options }) 26 | } 27 | } 28 | 29 | enum ParseJsonOption { 30 | UrlEncoded(bool), 31 | Preload(bool), 32 | } 33 | 34 | impl ParseJsonOption { 35 | fn apply_to_options(self, file: &mut FileAsset) { 36 | match self { 37 | ParseJsonOption::Preload(preload) => file.with_options_mut(|options| { 38 | if let FileOptions::Json(options) = options { 39 | options.set_preload(preload); 40 | } 41 | }), 42 | ParseJsonOption::UrlEncoded(url_encoded) => { 43 | file.set_url_encoded(url_encoded); 44 | } 45 | } 46 | } 47 | } 48 | 49 | impl Parse for ParseJsonOption { 50 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 51 | let _ = input.parse::()?; 52 | let ident = input.parse::()?; 53 | let _content; 54 | parenthesized!(_content in input); 55 | match ident.to_string().as_str() { 56 | "preload" => { 57 | crate::verify_preload_valid(&ident)?; 58 | Ok(ParseJsonOption::Preload(true)) 59 | }, 60 | "url_encoded" => Ok(ParseJsonOption::UrlEncoded(true)), 61 | _ => Err(syn::Error::new( 62 | proc_macro2::Span::call_site(), 63 | format!( 64 | "Unknown Json option: {}. Supported options are preload, url_encoded, and minify", 65 | ident 66 | ), 67 | )), 68 | } 69 | } 70 | } 71 | 72 | pub struct JsonAssetParser { 73 | file_name: Result, 74 | asset: AssetType, 75 | } 76 | 77 | impl Parse for JsonAssetParser { 78 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 79 | let inside; 80 | parenthesized!(inside in input); 81 | let path = inside.parse::()?; 82 | 83 | let parsed_options = { 84 | if input.is_empty() { 85 | None 86 | } else { 87 | Some(input.parse::()?) 88 | } 89 | }; 90 | 91 | let path_as_str = path.value(); 92 | let path: AssetSource = match AssetSource::parse_file(&path_as_str) { 93 | Ok(path) => path, 94 | Err(e) => { 95 | return Err(syn::Error::new( 96 | proc_macro2::Span::call_site(), 97 | format!("{e}"), 98 | )) 99 | } 100 | }; 101 | let mut this_file = FileAsset::new(path.clone()) 102 | .with_options(manganis_common::FileOptions::Json(Default::default())); 103 | if let Some(parsed_options) = parsed_options { 104 | parsed_options.apply_to_options(&mut this_file); 105 | } 106 | 107 | let asset = manganis_common::AssetType::File(this_file.clone()); 108 | 109 | let file_name = if this_file.url_encoded() { 110 | #[cfg(not(feature = "url-encoding"))] 111 | return Err(syn::Error::new( 112 | proc_macro2::Span::call_site(), 113 | "URL encoding is not enabled. Enable the url-encoding feature to use this feature", 114 | )); 115 | #[cfg(feature = "url-encoding")] 116 | Ok(crate::url_encoded_asset(&this_file).map_err(|e| { 117 | syn::Error::new( 118 | proc_macro2::Span::call_site(), 119 | format!("Failed to encode file: {}", e), 120 | ) 121 | })?) 122 | } else { 123 | this_file.served_location() 124 | }; 125 | 126 | Ok(JsonAssetParser { file_name, asset }) 127 | } 128 | } 129 | 130 | impl ToTokens for JsonAssetParser { 131 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 132 | let file_name = crate::quote_path(&self.file_name); 133 | 134 | let link_section = generate_link_section(self.asset.clone()); 135 | 136 | tokens.extend(quote! { 137 | { 138 | #link_section 139 | #file_name 140 | } 141 | }) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /macro/src/js.rs: -------------------------------------------------------------------------------- 1 | use manganis_common::{ 2 | AssetSource, AssetType, FileAsset, FileOptions, JsOptions, JsType, ManganisSupportError, 3 | }; 4 | use quote::{quote, ToTokens}; 5 | use syn::{parenthesized, parse::Parse, LitBool}; 6 | 7 | use crate::generate_link_section; 8 | 9 | struct ParseJsOptions { 10 | options: Vec, 11 | } 12 | 13 | impl ParseJsOptions { 14 | fn apply_to_options(self, file: &mut FileAsset) { 15 | for option in self.options { 16 | option.apply_to_options(file); 17 | } 18 | } 19 | } 20 | 21 | impl Parse for ParseJsOptions { 22 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 23 | let mut options = Vec::new(); 24 | while !input.is_empty() { 25 | options.push(input.parse::()?); 26 | } 27 | Ok(ParseJsOptions { options }) 28 | } 29 | } 30 | 31 | enum ParseJsOption { 32 | UrlEncoded(bool), 33 | Preload(bool), 34 | Minify(bool), 35 | } 36 | 37 | impl ParseJsOption { 38 | fn apply_to_options(self, file: &mut FileAsset) { 39 | match self { 40 | ParseJsOption::Preload(_) | ParseJsOption::Minify(_) => { 41 | file.with_options_mut(|options| { 42 | if let FileOptions::Js(options) = options { 43 | match self { 44 | ParseJsOption::Minify(format) => { 45 | options.set_minify(format); 46 | } 47 | ParseJsOption::Preload(preload) => { 48 | options.set_preload(preload); 49 | } 50 | _ => {} 51 | } 52 | } 53 | }) 54 | } 55 | ParseJsOption::UrlEncoded(url_encoded) => { 56 | file.set_url_encoded(url_encoded); 57 | } 58 | } 59 | } 60 | } 61 | 62 | impl Parse for ParseJsOption { 63 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 64 | let _ = input.parse::()?; 65 | let ident = input.parse::()?; 66 | let content; 67 | parenthesized!(content in input); 68 | match ident.to_string().as_str() { 69 | "preload" => { 70 | crate::verify_preload_valid(&ident)?; 71 | Ok(ParseJsOption::Preload(true)) 72 | } 73 | "url_encoded" => Ok(ParseJsOption::UrlEncoded(true)), 74 | "minify" => Ok(ParseJsOption::Minify(content.parse::()?.value())), 75 | _ => Err(syn::Error::new( 76 | proc_macro2::Span::call_site(), 77 | format!( 78 | "Unknown Js option: {}. Supported options are preload, url_encoded, and minify", 79 | ident 80 | ), 81 | )), 82 | } 83 | } 84 | } 85 | 86 | pub struct JsAssetParser { 87 | file_name: Result, 88 | asset: AssetType, 89 | } 90 | 91 | impl Parse for JsAssetParser { 92 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 93 | let inside; 94 | parenthesized!(inside in input); 95 | let path = inside.parse::()?; 96 | 97 | let parsed_options = { 98 | if input.is_empty() { 99 | None 100 | } else { 101 | Some(input.parse::()?) 102 | } 103 | }; 104 | 105 | let path_as_str = path.value(); 106 | let path: AssetSource = match AssetSource::parse_file(&path_as_str) { 107 | Ok(path) => path, 108 | Err(e) => { 109 | return Err(syn::Error::new( 110 | proc_macro2::Span::call_site(), 111 | format!("{e}"), 112 | )) 113 | } 114 | }; 115 | let mut this_file = FileAsset::new(path.clone()) 116 | .with_options(manganis_common::FileOptions::Js(JsOptions::new(JsType::Js))); 117 | if let Some(parsed_options) = parsed_options { 118 | parsed_options.apply_to_options(&mut this_file); 119 | } 120 | 121 | let asset = manganis_common::AssetType::File(this_file.clone()); 122 | 123 | let file_name = if this_file.url_encoded() { 124 | #[cfg(not(feature = "url-encoding"))] 125 | return Err(syn::Error::new( 126 | proc_macro2::Span::call_site(), 127 | "URL encoding is not enabled. Enable the url-encoding feature to use this feature", 128 | )); 129 | #[cfg(feature = "url-encoding")] 130 | Ok(crate::url_encoded_asset(&this_file).map_err(|e| { 131 | syn::Error::new( 132 | proc_macro2::Span::call_site(), 133 | format!("Failed to encode file: {}", e), 134 | ) 135 | })?) 136 | } else { 137 | this_file.served_location() 138 | }; 139 | 140 | Ok(JsAssetParser { file_name, asset }) 141 | } 142 | } 143 | 144 | impl ToTokens for JsAssetParser { 145 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 146 | let file_name = crate::quote_path(&self.file_name); 147 | 148 | let link_section = generate_link_section(self.asset.clone()); 149 | 150 | tokens.extend(quote! { 151 | { 152 | #link_section 153 | #file_name 154 | } 155 | }) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /macro/src/css.rs: -------------------------------------------------------------------------------- 1 | use manganis_common::{ 2 | AssetSource, AssetType, CssOptions, FileAsset, FileOptions, ManganisSupportError, 3 | }; 4 | use quote::{quote, ToTokens}; 5 | use syn::{parenthesized, parse::Parse, LitBool}; 6 | 7 | use crate::generate_link_section; 8 | 9 | struct ParseCssOptions { 10 | options: Vec, 11 | } 12 | 13 | impl ParseCssOptions { 14 | fn apply_to_options(self, file: &mut FileAsset) { 15 | for option in self.options { 16 | option.apply_to_options(file); 17 | } 18 | } 19 | } 20 | 21 | impl Parse for ParseCssOptions { 22 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 23 | let mut options = Vec::new(); 24 | while !input.is_empty() { 25 | options.push(input.parse::()?); 26 | } 27 | Ok(ParseCssOptions { options }) 28 | } 29 | } 30 | 31 | enum ParseCssOption { 32 | UrlEncoded(bool), 33 | Preload(bool), 34 | Minify(bool), 35 | } 36 | 37 | impl ParseCssOption { 38 | fn apply_to_options(self, file: &mut FileAsset) { 39 | match self { 40 | ParseCssOption::Preload(_) | ParseCssOption::Minify(_) => { 41 | file.with_options_mut(|options| { 42 | if let FileOptions::Css(options) = options { 43 | match self { 44 | ParseCssOption::Minify(format) => { 45 | options.set_minify(format); 46 | } 47 | ParseCssOption::Preload(preload) => { 48 | options.set_preload(preload); 49 | } 50 | _ => {} 51 | } 52 | } 53 | }) 54 | } 55 | ParseCssOption::UrlEncoded(url_encoded) => { 56 | file.set_url_encoded(url_encoded); 57 | } 58 | } 59 | } 60 | } 61 | 62 | impl Parse for ParseCssOption { 63 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 64 | let _ = input.parse::()?; 65 | let ident = input.parse::()?; 66 | let content; 67 | parenthesized!(content in input); 68 | match ident.to_string().as_str() { 69 | "preload" => { 70 | crate::verify_preload_valid(&ident)?; 71 | Ok(ParseCssOption::Preload(true)) 72 | } 73 | "url_encoded" => { 74 | Ok(ParseCssOption::UrlEncoded(true)) 75 | } 76 | "minify" => { 77 | Ok(ParseCssOption::Minify(content.parse::()?.value())) 78 | } 79 | _ => Err(syn::Error::new( 80 | proc_macro2::Span::call_site(), 81 | format!( 82 | "Unknown Css option: {}. Supported options are preload, url_encoded, and minify", 83 | ident 84 | ), 85 | )), 86 | } 87 | } 88 | } 89 | 90 | pub struct CssAssetParser { 91 | file_name: Result, 92 | asset: AssetType, 93 | } 94 | 95 | impl Parse for CssAssetParser { 96 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 97 | let inside; 98 | parenthesized!(inside in input); 99 | let path = inside.parse::()?; 100 | 101 | let parsed_options = { 102 | if input.is_empty() { 103 | None 104 | } else { 105 | Some(input.parse::()?) 106 | } 107 | }; 108 | 109 | let path_as_str = path.value(); 110 | let path: AssetSource = match AssetSource::parse_file(&path_as_str) { 111 | Ok(path) => path, 112 | Err(e) => { 113 | return Err(syn::Error::new( 114 | proc_macro2::Span::call_site(), 115 | format!("{e}"), 116 | )) 117 | } 118 | }; 119 | let mut this_file = FileAsset::new(path.clone()) 120 | .with_options(manganis_common::FileOptions::Css(CssOptions::new())); 121 | if let Some(parsed_options) = parsed_options { 122 | parsed_options.apply_to_options(&mut this_file); 123 | } 124 | 125 | let asset = manganis_common::AssetType::File(this_file.clone()); 126 | 127 | let file_name = if this_file.url_encoded() { 128 | #[cfg(not(feature = "url-encoding"))] 129 | return Err(syn::Error::new( 130 | proc_macro2::Span::call_site(), 131 | "URL encoding is not enabled. Enable the url-encoding feature to use this feature", 132 | )); 133 | #[cfg(feature = "url-encoding")] 134 | Ok(crate::url_encoded_asset(&this_file).map_err(|e| { 135 | syn::Error::new( 136 | proc_macro2::Span::call_site(), 137 | format!("Failed to encode file: {}", e), 138 | ) 139 | })?) 140 | } else { 141 | this_file.served_location() 142 | }; 143 | 144 | Ok(CssAssetParser { file_name, asset }) 145 | } 146 | } 147 | 148 | impl ToTokens for CssAssetParser { 149 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 150 | let file_name = crate::quote_path(&self.file_name); 151 | 152 | let link_section = generate_link_section(self.asset.clone()); 153 | 154 | tokens.extend(quote! { 155 | { 156 | #link_section 157 | #file_name 158 | } 159 | }) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /cli-support/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "manganis-cli-support" 3 | version.workspace = true 4 | edition = "2021" 5 | authors = ["Evan Almloff"] 6 | description = "Ergonomic, automatic, cross crate asset collection and optimization" 7 | license = "MIT OR Apache-2.0" 8 | repository = "https://github.com/DioxusLabs/manganis/" 9 | homepage = "https://dioxuslabs.com" 10 | keywords = ["assets"] 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | manganis-common = { path = "../common", version = "0.3.0-alpha.3" } 16 | 17 | serde = { version = "1.0.183", features = ["derive"] } 18 | serde_json = { version = "1.0.116" } 19 | anyhow = "1" 20 | rayon = "1.7.0" 21 | rustc-hash = "1.1.0" 22 | 23 | # Tailwind 24 | railwind = "0.1.5" 25 | 26 | # Image compression/conversion 27 | # JPEG 28 | mozjpeg = { version = "0.10.7", default-features = false, features = [ 29 | "parallel", 30 | ] } 31 | # PNG 32 | imagequant = "4.2.0" 33 | png = "0.17.9" 34 | # Conversion 35 | image = { version = "0.25" } 36 | ravif = { version = "0.11", default-features = false } 37 | 38 | # CSS Minification 39 | lightningcss = "1.0.0-alpha.44" 40 | 41 | # Js minification - swc has introduces minor versions with breaking changes in the past so we pin all of their crates 42 | swc = "=0.283.0" 43 | swc_allocator = { version = "=0.1.8", default-features = false } 44 | swc_atoms = { version = "=0.6.7", default-features = false } 45 | swc_cached = { version = "=0.3.20", default-features = false } 46 | swc_common = { version = "=0.37.5", default-features = false } 47 | swc_compiler_base = { version = "=0.19.0", default-features = false } 48 | swc_config = { version = "=0.1.15", default-features = false } 49 | swc_config_macro = { version = "=0.1.4", default-features = false } 50 | swc_ecma_ast = { version = "=0.118.2", default-features = false } 51 | swc_ecma_codegen = { version = "=0.155.1", default-features = false } 52 | swc_ecma_codegen_macros = { version = "=0.7.7", default-features = false } 53 | swc_ecma_compat_bugfixes = { version = "=0.12.0", default-features = false } 54 | swc_ecma_compat_common = { version = "=0.11.0", default-features = false } 55 | swc_ecma_compat_es2015 = { version = "=0.12.0", default-features = false } 56 | swc_ecma_compat_es2016 = { version = "=0.12.0", default-features = false } 57 | swc_ecma_compat_es2017 = { version = "=0.12.0", default-features = false } 58 | swc_ecma_compat_es2018 = { version = "=0.12.0", default-features = false } 59 | swc_ecma_compat_es2019 = { version = "=0.12.0", default-features = false } 60 | swc_ecma_compat_es2020 = { version = "=0.12.0", default-features = false } 61 | swc_ecma_compat_es2021 = { version = "=0.12.0", default-features = false } 62 | swc_ecma_compat_es2022 = { version = "=0.12.0", default-features = false } 63 | swc_ecma_compat_es3 = { version = "=0.12.0", default-features = false } 64 | swc_ecma_ext_transforms = { version = "=0.120.0", default-features = false } 65 | swc_ecma_lints = { version = "=0.100.0", default-features = false } 66 | swc_ecma_loader = { version = "=0.49.1", default-features = false } 67 | swc_ecma_minifier = { version = "=0.204.0", default-features = false } 68 | swc_ecma_parser = { version = "=0.149.1", default-features = false } 69 | swc_ecma_preset_env = { version = "=0.217.0", default-features = false, features = [ 70 | "serde", 71 | ] } 72 | swc_ecma_transforms = { version = "=0.239.0", default-features = false } 73 | swc_ecma_transforms_base = { version = "=0.145.0", default-features = false } 74 | swc_ecma_transforms_classes = { version = "=0.134.0", default-features = false } 75 | swc_ecma_transforms_compat = { version = "=0.171.0", default-features = false } 76 | swc_ecma_transforms_macros = { version = "=0.5.5", default-features = false } 77 | swc_ecma_transforms_module = { version = "=0.190.0", default-features = false } 78 | swc_ecma_transforms_optimization = { version = "=0.208.0", default-features = false } 79 | swc_ecma_transforms_proposal = { version = "=0.178.0", default-features = false } 80 | swc_ecma_transforms_react = { version = "=0.191.0", default-features = false } 81 | swc_ecma_transforms_typescript = { version = "=0.198.1", default-features = false } 82 | swc_ecma_usage_analyzer = { version = "=0.30.3", default-features = false } 83 | swc_ecma_utils = { version = "=0.134.2", default-features = false } 84 | swc_ecma_visit = { version = "=0.104.8", default-features = false } 85 | swc_eq_ignore_macros = { version = "=0.1.4", default-features = false } 86 | swc_error_reporters = { version = "=0.21.0", default-features = false } 87 | swc_fast_graph = { version = "=0.25.0", default-features = false } 88 | swc_macros_common = { version = "=0.3.13", default-features = false } 89 | swc_node_comments = { version = "=0.24.0", default-features = false } 90 | swc_timer = { version = "=0.25.0", default-features = false } 91 | swc_trace_macro = { version = "=0.1.3", default-features = false } 92 | swc_transform_common = { version = "=0.1.1", default-features = false } 93 | swc_typescript = { version = "=0.5.0", default-features = false } 94 | swc_visit = { version = "=0.6.2", default-features = false } 95 | 96 | # Remote assets 97 | url = { version = "2.4.0", features = ["serde"] } 98 | reqwest = { version = "0.12.5", features = ["blocking"] } 99 | tracing = "0.1.37" 100 | 101 | # Extracting data from an executable 102 | object = { version = "0.36.0", features = ["wasm"] } 103 | 104 | [dev-dependencies] 105 | tracing-subscriber = "0.3.18" 106 | 107 | [features] 108 | default = [] 109 | asm = ["ravif/asm", "mozjpeg/nasm_simd"] 110 | html = ["manganis-common/html"] 111 | # Note: this feature now enables nothing and should be removed in the next major version 112 | webp = [] 113 | avif = [] 114 | -------------------------------------------------------------------------------- /macro/src/font.rs: -------------------------------------------------------------------------------- 1 | use manganis_common::{AssetSource, AssetType, CssOptions, FileAsset, ManganisSupportError}; 2 | use quote::{quote, ToTokens}; 3 | use syn::{bracketed, parenthesized, parse::Parse}; 4 | 5 | use crate::generate_link_section; 6 | 7 | #[derive(Default)] 8 | struct FontFamilies { 9 | families: Vec, 10 | } 11 | 12 | impl Parse for FontFamilies { 13 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 14 | let inside; 15 | bracketed!(inside in input); 16 | let array = 17 | syn::punctuated::Punctuated::::parse_separated_nonempty( 18 | &inside, 19 | )?; 20 | Ok(FontFamilies { 21 | families: array.into_iter().map(|f| f.value()).collect(), 22 | }) 23 | } 24 | } 25 | 26 | #[derive(Default)] 27 | struct FontWeights { 28 | weights: Vec, 29 | } 30 | 31 | impl Parse for FontWeights { 32 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 33 | let inside; 34 | bracketed!(inside in input); 35 | let array = 36 | syn::punctuated::Punctuated::::parse_separated_nonempty( 37 | &inside, 38 | )?; 39 | Ok(FontWeights { 40 | weights: array 41 | .into_iter() 42 | .map(|f| f.base10_parse().unwrap()) 43 | .collect(), 44 | }) 45 | } 46 | } 47 | 48 | struct ParseFontOptions { 49 | families: FontFamilies, 50 | weights: FontWeights, 51 | text: Option, 52 | display: Option, 53 | } 54 | 55 | impl ParseFontOptions { 56 | fn url(&self) -> String { 57 | let mut segments = Vec::new(); 58 | 59 | let families: Vec<_> = self 60 | .families 61 | .families 62 | .iter() 63 | .map(|f| f.replace(' ', "+")) 64 | .collect(); 65 | if !families.is_empty() { 66 | segments.push(format!("family={}", families.join("&"))); 67 | } 68 | 69 | let weights: Vec<_> = self.weights.weights.iter().map(|w| w.to_string()).collect(); 70 | if !weights.is_empty() { 71 | segments.push(format!("weight={}", weights.join(","))); 72 | } 73 | 74 | if let Some(text) = &self.text { 75 | segments.push(format!("text={}", text.replace(' ', "+"))); 76 | } 77 | 78 | if let Some(display) = &self.display { 79 | segments.push(format!("display={}", display.replace(' ', "+"))); 80 | } 81 | 82 | let query = if segments.is_empty() { 83 | String::new() 84 | } else { 85 | format!("?{}", segments.join("&")) 86 | }; 87 | 88 | format!("https://fonts.googleapis.com/css2{}", query) 89 | } 90 | } 91 | 92 | impl Parse for ParseFontOptions { 93 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 94 | let mut families = None; 95 | let mut weights = None; 96 | let mut text = None; 97 | let mut display = None; 98 | loop { 99 | if input.is_empty() { 100 | break; 101 | } 102 | let _ = input.parse::()?; 103 | let ident = input.parse::()?; 104 | let inside; 105 | parenthesized!(inside in input); 106 | match ident.to_string().to_lowercase().as_str() { 107 | "families" => { 108 | families = Some(inside.parse::()?); 109 | } 110 | "weights" => { 111 | weights = Some(inside.parse::()?); 112 | } 113 | "text" => { 114 | text = Some(inside.parse::()?.value()); 115 | } 116 | "display" => { 117 | display = Some(inside.parse::()?.value()); 118 | } 119 | _ => { 120 | return Err(syn::Error::new( 121 | proc_macro2::Span::call_site(), 122 | format!("Unknown font option: {ident}. Supported options are families, weights, text, display"), 123 | )) 124 | } 125 | } 126 | } 127 | 128 | Ok(ParseFontOptions { 129 | families: families.unwrap_or_default(), 130 | weights: weights.unwrap_or_default(), 131 | text, 132 | display, 133 | }) 134 | } 135 | } 136 | 137 | pub struct FontAssetParser { 138 | file_name: Result, 139 | asset: AssetType, 140 | } 141 | 142 | impl Parse for FontAssetParser { 143 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 144 | let inside; 145 | parenthesized!(inside in input); 146 | if !inside.is_empty() { 147 | return Err(syn::Error::new( 148 | proc_macro2::Span::call_site(), 149 | "Font assets do not support paths. Please use file() if you want to import a local font file", 150 | )); 151 | } 152 | 153 | let options = input.parse::()?; 154 | 155 | let url = options.url(); 156 | let url: AssetSource = match AssetSource::parse_file(&url) { 157 | Ok(url) => url, 158 | Err(e) => { 159 | return Err(syn::Error::new( 160 | proc_macro2::Span::call_site(), 161 | format!("Failed to parse url: {url:?}\n{e}"), 162 | )) 163 | } 164 | }; 165 | let this_file = FileAsset::new(url.clone()) 166 | .with_options(manganis_common::FileOptions::Css(CssOptions::default())); 167 | let asset = manganis_common::AssetType::File(this_file.clone()); 168 | 169 | let file_name = this_file.served_location(); 170 | 171 | Ok(FontAssetParser { file_name, asset }) 172 | } 173 | } 174 | 175 | impl ToTokens for FontAssetParser { 176 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 177 | let file_name = crate::quote_path(&self.file_name); 178 | 179 | let link_section = generate_link_section(self.asset.clone()); 180 | 181 | tokens.extend(quote! { 182 | { 183 | #link_section 184 | #file_name 185 | } 186 | }) 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /cli-support/src/linker_intercept.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::OsStr, fs, path::PathBuf, process::Stdio}; 2 | 3 | // The prefix to link args passed from parent process. 4 | const MG_ARG_NAME: &str = "mg-arg="; 5 | 6 | /// Intercept the linker for object files. 7 | /// 8 | /// Takes the arguments used in a CLI and returns a list of paths to `.rlib` or `.o` files to be searched for asset sections. 9 | pub fn linker_intercept(args: I) -> Option<(Vec, Vec)> 10 | where 11 | I: IntoIterator, 12 | T: ToString, 13 | { 14 | let args: Vec = args.into_iter().map(|x| x.to_string()).collect(); 15 | 16 | // Check if we were provided with a command file. 17 | let mut is_command_file = None; 18 | for arg in args.iter() { 19 | // On windows the linker args are passed in a file that is referenced by `@` 20 | if arg.starts_with('@') { 21 | is_command_file = Some(arg.clone()); 22 | break; 23 | } 24 | } 25 | 26 | let linker_args = match is_command_file { 27 | // On unix/linux/mac the linker args are passed directly. 28 | None => args, 29 | // Handle windows here - uf16le and utf8 files are supported. 30 | Some(arg) => { 31 | let path = arg.trim().trim_start_matches('@'); 32 | let file_binary = fs::read(path).unwrap(); 33 | 34 | // This may be a utf-16le file. Let's try utf-8 first. 35 | let content = match String::from_utf8(file_binary.clone()) { 36 | Ok(s) => s, 37 | Err(_) => { 38 | // Convert Vec to Vec to convert into a String 39 | let binary_u16le: Vec = file_binary 40 | .chunks_exact(2) 41 | .map(|a| u16::from_le_bytes([a[0], a[1]])) 42 | .collect(); 43 | 44 | String::from_utf16_lossy(&binary_u16le) 45 | } 46 | }; 47 | 48 | // Gather linker args 49 | let mut linker_args = Vec::new(); 50 | let lines = content.lines(); 51 | 52 | for line in lines { 53 | // Remove quotes from the line - windows link args files are quoted 54 | let line_parsed = line.to_string(); 55 | let line_parsed = line_parsed.trim_end_matches('"').to_string(); 56 | let line_parsed = line_parsed.trim_start_matches('"').to_string(); 57 | 58 | linker_args.push(line_parsed); 59 | } 60 | 61 | linker_args 62 | } 63 | }; 64 | 65 | let mut link_args = Vec::new(); 66 | 67 | // Parse through linker args for `.o` or `.rlib` files. 68 | let mut object_files: Vec = Vec::new(); 69 | for item in linker_args { 70 | // Get the working directory so it isn't lost. 71 | // When rust calls the linker it doesn't pass the working dir so we need to recover it. 72 | // "{MG_WORKDIR_ARG_NAME}path" 73 | if item.starts_with(MG_ARG_NAME) { 74 | let split: Vec<_> = item.split('=').collect(); 75 | link_args.push(split[1].to_string()); 76 | continue; 77 | } 78 | 79 | if item.ends_with(".o") || item.ends_with(".rlib") { 80 | object_files.push(PathBuf::from(item)); 81 | } 82 | } 83 | 84 | if object_files.is_empty() { 85 | return None; 86 | } 87 | 88 | Some((link_args, object_files)) 89 | } 90 | 91 | /// Calls cargo to build the project with a linker intercept script. 92 | /// 93 | /// The linker intercept script will call the current executable with the specified subcommand 94 | /// and a list of arguments provided by rustc. 95 | pub fn start_linker_intercept( 96 | subcommand: &str, 97 | args: I, 98 | link_args: Option, 99 | ) -> Result<(), std::io::Error> 100 | where 101 | I: IntoIterator, 102 | I::Item: AsRef, 103 | J: IntoIterator, 104 | J::Item: ToString, 105 | { 106 | let exec_path = std::env::current_exe().unwrap(); 107 | 108 | let mut cmd = std::process::Command::new("cargo"); 109 | cmd.arg("rustc"); 110 | cmd.args(args); 111 | cmd.arg("--"); 112 | 113 | // Build a temporary redirect script. 114 | let script_path = create_linker_script(exec_path, subcommand).unwrap(); 115 | let linker_arg = format!("-Clinker={}", script_path.display()); 116 | cmd.arg(linker_arg); 117 | 118 | // Handle passing any arguments back to the current executable. 119 | if let Some(link_args) = link_args { 120 | let link_args: Vec = link_args.into_iter().map(|x| x.to_string()).collect(); 121 | for link_arg in link_args { 122 | let arg = format!("-Clink-arg={}{}", MG_ARG_NAME, link_arg); 123 | cmd.arg(arg); 124 | } 125 | } 126 | 127 | cmd.stdout(Stdio::piped()) 128 | .stderr(Stdio::piped()) 129 | .spawn()? 130 | .wait()?; 131 | Ok(()) 132 | } 133 | 134 | const LINK_SCRIPT_NAME: &str = "mg-link"; 135 | 136 | /// Creates a temporary script that re-routes rustc linker args to a subcommand of an executable. 137 | fn create_linker_script(exec: PathBuf, subcommand: &str) -> Result { 138 | #[cfg(windows)] 139 | let (script, ext) = ( 140 | format!("echo off\n{} {} %*", exec.display(), subcommand), 141 | "bat", 142 | ); 143 | #[cfg(not(windows))] 144 | let (script, ext) = ( 145 | format!("#!/usr/bin/env bash\n{} {} $@", exec.display(), subcommand), 146 | "sh", 147 | ); 148 | 149 | let temp_path = std::env::temp_dir(); 150 | let out_name = format!("{LINK_SCRIPT_NAME}.{ext}"); 151 | let out = temp_path.join(out_name); 152 | fs::write(&out, script)?; 153 | 154 | // Set executable permissions. 155 | let mut perms = fs::metadata(&out)?.permissions(); 156 | 157 | // We give windows RW and implied X perms. 158 | // Clippy complains on any platform about this even if it's not *nix. 159 | // https://rust-lang.github.io/rust-clippy/master/index.html#permissions_set_readonly_false 160 | #[cfg(windows)] 161 | #[allow(clippy::permissions_set_readonly_false)] 162 | perms.set_readonly(false); 163 | 164 | // We give nix user-RWX perms. 165 | #[cfg(not(windows))] 166 | { 167 | use std::os::unix::fs::PermissionsExt; 168 | perms.set_mode(0o700); 169 | } 170 | fs::set_permissions(&out, perms)?; 171 | 172 | Ok(out) 173 | } 174 | 175 | /// Deletes the temporary script created by `create_linker_script`. 176 | pub fn delete_linker_script() -> Result<(), std::io::Error> { 177 | #[cfg(windows)] 178 | let ext = "bat"; 179 | #[cfg(not(windows))] 180 | let ext = "sh"; 181 | 182 | let temp_path = std::env::temp_dir(); 183 | let file_name = format!("{LINK_SCRIPT_NAME}.{ext}"); 184 | let file = temp_path.join(file_name); 185 | fs::remove_file(file) 186 | } 187 | -------------------------------------------------------------------------------- /cli-support/src/manifest.rs: -------------------------------------------------------------------------------- 1 | pub use railwind::warning::Warning as TailwindWarning; 2 | use std::path::PathBuf; 3 | 4 | use manganis_common::{linker, AssetManifest, AssetType}; 5 | 6 | use crate::{file::process_file, process_folder}; 7 | 8 | use object::{File, Object, ObjectSection}; 9 | use std::fs; 10 | 11 | // get the text containing all the asset descriptions 12 | // in the "link section" of the binary 13 | fn get_string_manganis(file: &File) -> Option { 14 | for section in file.sections() { 15 | if let Ok(section_name) = section.name() { 16 | // Check if the link section matches the asset section for one of the platforms we support. This may not be the current platform if the user is cross compiling 17 | if linker::LinkSection::ALL 18 | .iter() 19 | .any(|x| x.link_section == section_name) 20 | { 21 | let bytes = section.uncompressed_data().ok()?; 22 | // Some platforms (e.g. macOS) start the manganis section with a null byte, we need to filter that out before we deserialize the JSON 23 | return Some( 24 | std::str::from_utf8(&bytes) 25 | .ok()? 26 | .chars() 27 | .filter(|c| !c.is_control()) 28 | .collect::(), 29 | ); 30 | } 31 | } 32 | } 33 | None 34 | } 35 | 36 | /// An extension trait CLI support for the asset manifest 37 | pub trait AssetManifestExt { 38 | /// Load a manifest from a list of Manganis JSON strings. 39 | /// 40 | /// The asset descriptions are stored inside a manifest file that is produced when the linker is intercepted. 41 | fn load(json: Vec) -> Self; 42 | /// Load a manifest from the assets propogated through object files. 43 | /// 44 | /// The asset descriptions are stored inside a manifest file that is produced when the linker is intercepted. 45 | fn load_from_objects(object_paths: Vec) -> Self; 46 | /// Optimize and copy all assets in the manifest to a folder 47 | fn copy_static_assets_to(&self, location: impl Into) -> anyhow::Result<()>; 48 | /// Collect all tailwind classes and generate string with the output css 49 | fn collect_tailwind_css( 50 | &self, 51 | include_preflight: bool, 52 | warnings: &mut Vec, 53 | ) -> String; 54 | } 55 | 56 | impl AssetManifestExt for AssetManifest { 57 | fn load(json: Vec) -> Self { 58 | let mut all_assets = Vec::new(); 59 | 60 | // Collect all assets for each manganis string found. 61 | for item in json { 62 | let mut assets = deserialize_assets(item.as_str()); 63 | all_assets.append(&mut assets); 64 | } 65 | 66 | // If we don't see any manganis assets used in the binary, just return an empty manifest 67 | if all_assets.is_empty() { 68 | return Self::default(); 69 | }; 70 | 71 | Self::new(all_assets) 72 | } 73 | 74 | fn load_from_objects(object_files: Vec) -> Self { 75 | let json = get_json_from_object_files(object_files); 76 | Self::load(json) 77 | } 78 | 79 | fn copy_static_assets_to(&self, location: impl Into) -> anyhow::Result<()> { 80 | let location = location.into(); 81 | match std::fs::create_dir_all(&location) { 82 | Ok(_) => {} 83 | Err(err) => { 84 | tracing::error!("Failed to create directory for static assets: {}", err); 85 | return Err(err.into()); 86 | } 87 | } 88 | 89 | self.assets().iter().try_for_each(|asset| { 90 | match asset { 91 | AssetType::File(file_asset) => { 92 | tracing::info!("Optimizing and bundling {}", file_asset); 93 | tracing::trace!("Copying asset from {:?} to {:?}", file_asset, location); 94 | match process_file(file_asset, &location) { 95 | Ok(_) => {} 96 | Err(err) => { 97 | tracing::error!("Failed to copy static asset: {}", err); 98 | return Err(err); 99 | } 100 | } 101 | } 102 | AssetType::Folder(folder_asset) => { 103 | tracing::info!("Copying folder asset {}", folder_asset); 104 | match process_folder(folder_asset, &location) { 105 | Ok(_) => {} 106 | Err(err) => { 107 | tracing::error!("Failed to copy static asset: {}", err); 108 | return Err(err); 109 | } 110 | } 111 | } 112 | _ => {} 113 | } 114 | Ok::<(), anyhow::Error>(()) 115 | }) 116 | } 117 | 118 | fn collect_tailwind_css( 119 | self: &AssetManifest, 120 | include_preflight: bool, 121 | warnings: &mut Vec, 122 | ) -> String { 123 | let mut all_classes = String::new(); 124 | 125 | for asset in self.assets() { 126 | if let AssetType::Tailwind(classes) = asset { 127 | all_classes.push_str(classes.classes()); 128 | all_classes.push(' '); 129 | } 130 | } 131 | 132 | let source = railwind::Source::String(all_classes, railwind::CollectionOptions::String); 133 | 134 | let css = railwind::parse_to_string(source, include_preflight, warnings); 135 | 136 | crate::file::minify_css(&css) 137 | } 138 | } 139 | 140 | fn deserialize_assets(json: &str) -> Vec { 141 | let deserializer = serde_json::Deserializer::from_str(json); 142 | deserializer 143 | .into_iter::() 144 | .map(|x| x.unwrap()) 145 | .collect() 146 | } 147 | 148 | /// Extract JSON Manganis strings from a list of object files. 149 | pub fn get_json_from_object_files(object_paths: Vec) -> Vec { 150 | let mut all_json = Vec::new(); 151 | 152 | for path in object_paths { 153 | let Some(ext) = path.extension() else { 154 | continue; 155 | }; 156 | 157 | let Some(ext) = ext.to_str() else { 158 | continue; 159 | }; 160 | 161 | let is_rlib = match ext { 162 | "rlib" => true, 163 | "o" => false, 164 | _ => continue, 165 | }; 166 | 167 | // Read binary data and try getting assets from manganis string 168 | let binary_data = fs::read(path).unwrap(); 169 | 170 | // rlibs are archives with object files inside. 171 | let mut data = match is_rlib { 172 | false => { 173 | // Parse an unarchived object file. We use a Vec to match the return types. 174 | let file = object::File::parse(&*binary_data).unwrap(); 175 | let mut data = Vec::new(); 176 | if let Some(string) = get_string_manganis(&file) { 177 | data.push(string); 178 | } 179 | data 180 | } 181 | true => { 182 | let file = object::read::archive::ArchiveFile::parse(&*binary_data).unwrap(); 183 | 184 | // rlibs can contain many object files so we collect each manganis string here. 185 | let mut manganis_strings = Vec::new(); 186 | 187 | // Look through each archive member for object files. 188 | // Read the archive member's binary data (we know it's an object file) 189 | // And parse it with the normal `object::File::parse` to find the manganis string. 190 | for member in file.members() { 191 | let member = member.unwrap(); 192 | let name = String::from_utf8_lossy(member.name()).to_string(); 193 | 194 | // Check if the archive member is an object file and parse it. 195 | if name.ends_with(".o") { 196 | let data = member.data(&*binary_data).unwrap(); 197 | let o_file = object::File::parse(data).unwrap(); 198 | if let Some(manganis_str) = get_string_manganis(&o_file) { 199 | manganis_strings.push(manganis_str); 200 | } 201 | } 202 | } 203 | 204 | manganis_strings 205 | } 206 | }; 207 | 208 | all_json.append(&mut data); 209 | } 210 | 211 | all_json 212 | } 213 | -------------------------------------------------------------------------------- /cli-support/src/file.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use image::{DynamicImage, EncodableLayout}; 3 | use lightningcss::stylesheet::{MinifyOptions, ParserOptions, PrinterOptions, StyleSheet}; 4 | use manganis_common::{ 5 | AssetSource, CssOptions, FileAsset, FileOptions, ImageOptions, ImageType, JsOptions, 6 | JsonOptions, 7 | }; 8 | use std::{ 9 | io::{BufWriter, Write}, 10 | path::Path, 11 | sync::Arc, 12 | }; 13 | use swc::{config::JsMinifyOptions, try_with_handler, BoolOrDataConfig}; 14 | use swc_common::{sync::Lrc, FileName}; 15 | use swc_common::{SourceMap, GLOBALS}; 16 | 17 | pub trait Process { 18 | fn process(&self, source: &AssetSource, output_path: &Path) -> anyhow::Result<()>; 19 | } 20 | 21 | /// Process a specific file asset 22 | pub fn process_file(file: &FileAsset, output_folder: &Path) -> anyhow::Result<()> { 23 | let location = file.location(); 24 | let source = location.source(); 25 | let output_path = output_folder.join(location.unique_name()); 26 | file.options().process(source, &output_path) 27 | } 28 | 29 | impl Process for FileOptions { 30 | fn process(&self, source: &AssetSource, output_path: &Path) -> anyhow::Result<()> { 31 | if output_path.exists() { 32 | return Ok(()); 33 | } 34 | match self { 35 | Self::Other { .. } => { 36 | let bytes = source.read_to_bytes()?; 37 | std::fs::write(output_path, bytes).with_context(|| { 38 | format!( 39 | "Failed to write file to output location: {}", 40 | output_path.display() 41 | ) 42 | })?; 43 | } 44 | Self::Css(options) => { 45 | options.process(source, output_path)?; 46 | } 47 | Self::Js(options) => { 48 | options.process(source, output_path)?; 49 | } 50 | Self::Json(options) => { 51 | options.process(source, output_path)?; 52 | } 53 | Self::Image(options) => { 54 | options.process(source, output_path)?; 55 | } 56 | _ => todo!(), 57 | } 58 | 59 | Ok(()) 60 | } 61 | } 62 | 63 | impl Process for ImageOptions { 64 | fn process(&self, source: &AssetSource, output_path: &Path) -> anyhow::Result<()> { 65 | let mut image = image::ImageReader::new(std::io::Cursor::new(&*source.read_to_bytes()?)) 66 | .with_guessed_format()? 67 | .decode()?; 68 | 69 | if let Some(size) = self.size() { 70 | image = image.resize_exact(size.0, size.1, image::imageops::FilterType::Lanczos3); 71 | } 72 | 73 | match self.ty() { 74 | ImageType::Png => { 75 | compress_png(image, output_path); 76 | } 77 | ImageType::Jpg => { 78 | compress_jpg(image, output_path)?; 79 | } 80 | ImageType::Avif => { 81 | if let Err(error) = image.save(output_path) { 82 | tracing::error!("Failed to save avif image: {} with path {}. You must have the avif feature enabled to use avif assets", error, output_path.display()); 83 | } 84 | } 85 | ImageType::Webp => { 86 | if let Err(err) = image.save(output_path) { 87 | tracing::error!("Failed to save webp image: {}. You must have the avif feature enabled to use webp assets", err); 88 | } 89 | } 90 | } 91 | 92 | Ok(()) 93 | } 94 | } 95 | 96 | fn compress_jpg(image: DynamicImage, output_location: &Path) -> anyhow::Result<()> { 97 | let mut comp = mozjpeg::Compress::new(mozjpeg::ColorSpace::JCS_EXT_RGBX); 98 | let width = image.width() as usize; 99 | let height = image.height() as usize; 100 | 101 | comp.set_size(width, height); 102 | let mut comp = comp.start_compress(Vec::new())?; // any io::Write will work 103 | 104 | comp.write_scanlines(image.to_rgba8().as_bytes())?; 105 | 106 | let jpeg_bytes = comp.finish()?; 107 | 108 | let file = std::fs::File::create(output_location)?; 109 | let w = &mut BufWriter::new(file); 110 | w.write_all(&jpeg_bytes)?; 111 | Ok(()) 112 | } 113 | 114 | fn compress_png(image: DynamicImage, output_location: &Path) { 115 | // Image loading/saving is outside scope of this library 116 | let width = image.width() as usize; 117 | let height = image.height() as usize; 118 | let bitmap: Vec<_> = image 119 | .into_rgba8() 120 | .pixels() 121 | .map(|px| imagequant::RGBA::new(px[0], px[1], px[2], px[3])) 122 | .collect(); 123 | 124 | // Configure the library 125 | let mut liq = imagequant::new(); 126 | liq.set_speed(5).unwrap(); 127 | liq.set_quality(0, 99).unwrap(); 128 | 129 | // Describe the bitmap 130 | let mut img = liq.new_image(&bitmap[..], width, height, 0.0).unwrap(); 131 | 132 | // The magic happens in quantize() 133 | let mut res = match liq.quantize(&mut img) { 134 | Ok(res) => res, 135 | Err(err) => panic!("Quantization failed, because: {err:?}"), 136 | }; 137 | 138 | let (palette, pixels) = res.remapped(&mut img).unwrap(); 139 | 140 | let file = std::fs::File::create(output_location).unwrap(); 141 | let w = &mut BufWriter::new(file); 142 | 143 | let mut encoder = png::Encoder::new(w, width as u32, height as u32); 144 | encoder.set_color(png::ColorType::Rgba); 145 | let mut flattened_palette = Vec::new(); 146 | let mut alpha_palette = Vec::new(); 147 | for px in palette { 148 | flattened_palette.push(px.r); 149 | flattened_palette.push(px.g); 150 | flattened_palette.push(px.b); 151 | alpha_palette.push(px.a); 152 | } 153 | encoder.set_palette(flattened_palette); 154 | encoder.set_trns(alpha_palette); 155 | encoder.set_depth(png::BitDepth::Eight); 156 | encoder.set_color(png::ColorType::Indexed); 157 | encoder.set_compression(png::Compression::Best); 158 | let mut writer = encoder.write_header().unwrap(); 159 | writer.write_image_data(&pixels).unwrap(); 160 | writer.finish().unwrap(); 161 | } 162 | 163 | impl Process for CssOptions { 164 | fn process(&self, source: &AssetSource, output_path: &Path) -> anyhow::Result<()> { 165 | let css = source.read_to_string()?; 166 | 167 | let css = if self.minify() { minify_css(&css) } else { css }; 168 | 169 | std::fs::write(output_path, css).with_context(|| { 170 | format!( 171 | "Failed to write css to output location: {}", 172 | output_path.display() 173 | ) 174 | })?; 175 | 176 | Ok(()) 177 | } 178 | } 179 | 180 | pub(crate) fn minify_css(css: &str) -> String { 181 | let mut stylesheet = StyleSheet::parse(css, ParserOptions::default()).unwrap(); 182 | stylesheet.minify(MinifyOptions::default()).unwrap(); 183 | let printer = PrinterOptions { 184 | minify: true, 185 | ..Default::default() 186 | }; 187 | let res = stylesheet.to_css(printer).unwrap(); 188 | res.code 189 | } 190 | 191 | pub(crate) fn minify_js(source: &AssetSource) -> anyhow::Result { 192 | let cm = Arc::::default(); 193 | 194 | let js = source.read_to_string()?; 195 | let c = swc::Compiler::new(cm.clone()); 196 | let output = GLOBALS 197 | .set(&Default::default(), || { 198 | try_with_handler(cm.clone(), Default::default(), |handler| { 199 | let filename = Lrc::new(match source { 200 | manganis_common::AssetSource::Local(path) => FileName::Real(path.clone()), 201 | manganis_common::AssetSource::Remote(url) => FileName::Url(url.clone()), 202 | }); 203 | let fm = cm.new_source_file(filename, js.to_string()); 204 | 205 | c.minify( 206 | fm, 207 | handler, 208 | &JsMinifyOptions { 209 | compress: BoolOrDataConfig::from_bool(true), 210 | mangle: BoolOrDataConfig::from_bool(true), 211 | ..Default::default() 212 | }, 213 | ) 214 | .context("failed to minify javascript") 215 | }) 216 | }) 217 | .map(|output| output.code); 218 | 219 | match output { 220 | Ok(output) => Ok(output), 221 | Err(err) => { 222 | tracing::error!("Failed to minify javascript: {}", err); 223 | Ok(js) 224 | } 225 | } 226 | } 227 | 228 | impl Process for JsOptions { 229 | fn process(&self, source: &AssetSource, output_path: &Path) -> anyhow::Result<()> { 230 | let js = if self.minify() { 231 | minify_js(source)? 232 | } else { 233 | source.read_to_string()? 234 | }; 235 | 236 | std::fs::write(output_path, js).with_context(|| { 237 | format!( 238 | "Failed to write js to output location: {}", 239 | output_path.display() 240 | ) 241 | })?; 242 | 243 | Ok(()) 244 | } 245 | } 246 | 247 | pub(crate) fn minify_json(source: &str) -> anyhow::Result { 248 | // First try to parse the json 249 | let json: serde_json::Value = serde_json::from_str(source)?; 250 | // Then print it in a minified format 251 | let json = serde_json::to_string(&json)?; 252 | Ok(json) 253 | } 254 | 255 | impl Process for JsonOptions { 256 | fn process(&self, source: &AssetSource, output_path: &Path) -> anyhow::Result<()> { 257 | let source = source.read_to_string()?; 258 | let json = match minify_json(&source) { 259 | Ok(json) => json, 260 | Err(err) => { 261 | tracing::error!("Failed to minify json: {}", err); 262 | source 263 | } 264 | }; 265 | 266 | std::fs::write(output_path, json).with_context(|| { 267 | format!( 268 | "Failed to write json to output location: {}", 269 | output_path.display() 270 | ) 271 | })?; 272 | 273 | Ok(()) 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /macro/src/image.rs: -------------------------------------------------------------------------------- 1 | use manganis_common::ManganisSupportError; 2 | use manganis_common::{AssetSource, AssetType, FileAsset, FileOptions, ImageOptions}; 3 | use quote::{quote, ToTokens}; 4 | use syn::{parenthesized, parse::Parse, Token}; 5 | 6 | use crate::generate_link_section; 7 | 8 | struct ParseImageOptions { 9 | options: Vec, 10 | } 11 | 12 | impl ParseImageOptions { 13 | fn apply_to_options(self, file: &mut FileAsset, low_quality_preview: &mut bool) { 14 | for option in self.options { 15 | option.apply_to_options(file, low_quality_preview); 16 | } 17 | } 18 | } 19 | 20 | impl Parse for ParseImageOptions { 21 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 22 | let mut options = Vec::new(); 23 | while !input.is_empty() { 24 | options.push(input.parse::()?); 25 | } 26 | Ok(ParseImageOptions { options }) 27 | } 28 | } 29 | 30 | enum ParseImageOption { 31 | Format(manganis_common::ImageType), 32 | Size((u32, u32)), 33 | Preload(bool), 34 | UrlEncoded(bool), 35 | Lqip(bool), 36 | } 37 | 38 | impl ParseImageOption { 39 | fn apply_to_options(self, file: &mut FileAsset, low_quality_preview: &mut bool) { 40 | match self { 41 | ParseImageOption::Format(_) 42 | | ParseImageOption::Size(_) 43 | | ParseImageOption::Preload(_) => file.with_options_mut(|options| { 44 | if let FileOptions::Image(options) = options { 45 | match self { 46 | ParseImageOption::Format(format) => { 47 | options.set_ty(format); 48 | } 49 | ParseImageOption::Size(size) => { 50 | options.set_size(Some(size)); 51 | } 52 | ParseImageOption::Preload(preload) => { 53 | options.set_preload(preload); 54 | } 55 | _ => {} 56 | } 57 | } 58 | }), 59 | ParseImageOption::UrlEncoded(url_encoded) => { 60 | file.set_url_encoded(url_encoded); 61 | } 62 | ParseImageOption::Lqip(lqip) => { 63 | *low_quality_preview = lqip; 64 | } 65 | } 66 | } 67 | } 68 | 69 | impl Parse for ParseImageOption { 70 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 71 | let _ = input.parse::()?; 72 | let ident = input.parse::()?; 73 | let content; 74 | parenthesized!(content in input); 75 | match ident.to_string().as_str() { 76 | "format" => { 77 | let format = content.parse::()?; 78 | Ok(ParseImageOption::Format(format.into())) 79 | } 80 | "size" => { 81 | let size = content.parse::()?; 82 | Ok(ParseImageOption::Size((size.width, size.height))) 83 | } 84 | "preload" => { 85 | crate::verify_preload_valid(&ident)?; 86 | Ok(ParseImageOption::Preload(true)) 87 | } 88 | "url_encoded" => { 89 | Ok(ParseImageOption::UrlEncoded(true)) 90 | } 91 | "low_quality_preview" => { 92 | Ok(ParseImageOption::Lqip(true)) 93 | } 94 | _ => Err(syn::Error::new( 95 | proc_macro2::Span::call_site(), 96 | format!( 97 | "Unknown image option: {}. Supported options are format, size, preload, url_encoded, low_quality_preview", 98 | ident 99 | ), 100 | )), 101 | } 102 | } 103 | } 104 | 105 | struct ImageSize { 106 | width: u32, 107 | height: u32, 108 | } 109 | 110 | impl Parse for ImageSize { 111 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 112 | let width = input.parse::()?; 113 | let _ = input.parse::()?; 114 | let height = input.parse::()?; 115 | Ok(ImageSize { 116 | width: width.base10_parse()?, 117 | height: height.base10_parse()?, 118 | }) 119 | } 120 | } 121 | 122 | impl From for manganis_common::ImageType { 123 | fn from(val: ImageType) -> Self { 124 | val.0 125 | } 126 | } 127 | 128 | impl Parse for ImageType { 129 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 130 | let _ = input.parse::()?; 131 | let _ = input.parse::()?; 132 | let ident = input.parse::()?; 133 | ident 134 | .to_string() 135 | .to_lowercase() 136 | .as_str() 137 | .parse::() 138 | .map_err(|_| { 139 | syn::Error::new( 140 | proc_macro2::Span::call_site(), 141 | format!( 142 | "Unknown image type: {}. Supported types are png, jpeg, webp, avif", 143 | ident 144 | ), 145 | ) 146 | }) 147 | .map(Self) 148 | } 149 | } 150 | 151 | #[derive(Clone, Copy)] 152 | struct ImageType(manganis_common::ImageType); 153 | 154 | impl Default for ImageType { 155 | fn default() -> Self { 156 | Self(manganis_common::ImageType::Avif) 157 | } 158 | } 159 | 160 | pub struct ImageAssetParser { 161 | file_name: Result, 162 | low_quality_preview: Option, 163 | asset: AssetType, 164 | } 165 | 166 | impl Parse for ImageAssetParser { 167 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 168 | let inside; 169 | parenthesized!(inside in input); 170 | let path = inside.parse::()?; 171 | 172 | let parsed_options = { 173 | if input.is_empty() { 174 | None 175 | } else { 176 | Some(input.parse::()?) 177 | } 178 | }; 179 | 180 | let path_as_str = path.value(); 181 | let path: AssetSource = match AssetSource::parse_file(&path_as_str) { 182 | Ok(path) => path, 183 | Err(e) => { 184 | return Err(syn::Error::new( 185 | proc_macro2::Span::call_site(), 186 | format!("{e}"), 187 | )) 188 | } 189 | }; 190 | let mut this_file = 191 | FileAsset::new(path.clone()).with_options(manganis_common::FileOptions::Image( 192 | ImageOptions::new(manganis_common::ImageType::Avif, None), 193 | )); 194 | let mut low_quality_preview = false; 195 | if let Some(parsed_options) = parsed_options { 196 | parsed_options.apply_to_options(&mut this_file, &mut low_quality_preview); 197 | } 198 | 199 | let asset = manganis_common::AssetType::File(this_file.clone()); 200 | 201 | let file_name = if this_file.url_encoded() { 202 | #[cfg(not(feature = "url-encoding"))] 203 | return Err(syn::Error::new( 204 | proc_macro2::Span::call_site(), 205 | "URL encoding is not enabled. Enable the url-encoding feature to use this feature", 206 | )); 207 | #[cfg(feature = "url-encoding")] 208 | Ok(crate::url_encoded_asset(&this_file).map_err(|e| { 209 | syn::Error::new( 210 | proc_macro2::Span::call_site(), 211 | format!("Failed to encode file: {}", e), 212 | ) 213 | })?) 214 | } else { 215 | this_file.served_location() 216 | }; 217 | 218 | let low_quality_preview = if low_quality_preview { 219 | #[cfg(not(feature = "url-encoding"))] 220 | return Err(syn::Error::new( 221 | proc_macro2::Span::call_site(), 222 | "Low quality previews require URL encoding. Enable the url-encoding feature to use this feature", 223 | )); 224 | 225 | #[cfg(feature = "url-encoding")] 226 | { 227 | let current_image_size = match this_file.options() { 228 | manganis_common::FileOptions::Image(options) => options.size(), 229 | _ => None, 230 | }; 231 | let low_quality_preview_size = current_image_size 232 | .map(|(width, height)| { 233 | let width = width / 10; 234 | let height = height / 10; 235 | (width, height) 236 | }) 237 | .unwrap_or((32, 32)); 238 | let lqip = FileAsset::new(path).with_options(manganis_common::FileOptions::Image( 239 | ImageOptions::new( 240 | manganis_common::ImageType::Avif, 241 | Some(low_quality_preview_size), 242 | ), 243 | )); 244 | 245 | Some(crate::url_encoded_asset(&lqip).map_err(|e| { 246 | syn::Error::new( 247 | proc_macro2::Span::call_site(), 248 | format!("Failed to encode file: {}", e), 249 | ) 250 | })?) 251 | } 252 | } else { 253 | None 254 | }; 255 | 256 | Ok(ImageAssetParser { 257 | file_name, 258 | low_quality_preview, 259 | asset, 260 | }) 261 | } 262 | } 263 | 264 | impl ToTokens for ImageAssetParser { 265 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 266 | let file_name = crate::quote_path(&self.file_name); 267 | let low_quality_preview = match &self.low_quality_preview { 268 | Some(lqip) => quote! { Some(#lqip) }, 269 | None => quote! { None }, 270 | }; 271 | 272 | let link_section = generate_link_section(self.asset.clone()); 273 | 274 | tokens.extend(quote! { 275 | { 276 | #link_section 277 | manganis::ImageAsset::new(#file_name).with_preview(#low_quality_preview) 278 | } 279 | }) 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![deny(missing_docs)] 3 | 4 | use css::CssAssetParser; 5 | use file::FileAssetParser; 6 | use folder::FolderAssetParser; 7 | use font::FontAssetParser; 8 | use image::ImageAssetParser; 9 | use js::JsAssetParser; 10 | use json::JsonAssetParser; 11 | use manganis_common::cache::macro_log_file; 12 | use manganis_common::{AssetSource, MetadataAsset, TailwindAsset}; 13 | use proc_macro::TokenStream; 14 | use proc_macro2::Ident; 15 | use proc_macro2::TokenStream as TokenStream2; 16 | use quote::{quote, quote_spanned, ToTokens}; 17 | use std::sync::atomic::AtomicBool; 18 | use std::sync::atomic::Ordering; 19 | use syn::{parse::Parse, parse_macro_input, LitStr}; 20 | 21 | mod css; 22 | mod file; 23 | mod folder; 24 | mod font; 25 | mod image; 26 | mod js; 27 | mod json; 28 | 29 | static LOG_FILE_FRESH: AtomicBool = AtomicBool::new(false); 30 | 31 | fn trace_to_file() { 32 | // If this is the first time the macro is used in the crate, set the subscriber to write to a file 33 | if !LOG_FILE_FRESH.fetch_or(true, Ordering::Relaxed) { 34 | let path = macro_log_file(); 35 | std::fs::create_dir_all(path.parent().unwrap()).unwrap(); 36 | let file = std::fs::OpenOptions::new() 37 | .create(true) 38 | .write(true) 39 | .truncate(true) 40 | .open(path) 41 | .unwrap(); 42 | tracing_subscriber::fmt::fmt().with_writer(file).init(); 43 | } 44 | } 45 | 46 | /// this new approach will store the assets descriptions *inside the executable*. 47 | /// The trick is to use the `link_section` attribute. 48 | /// We force rust to store a json representation of the asset description 49 | /// inside a particular region of the binary, with the label "manganis". 50 | /// After linking, the "manganis" sections of the different executables will be merged. 51 | fn generate_link_section(asset: manganis_common::AssetType) -> TokenStream2 { 52 | let position = proc_macro2::Span::call_site(); 53 | 54 | let asset_description = serde_json::to_string(&asset).unwrap(); 55 | 56 | let len = asset_description.as_bytes().len(); 57 | 58 | let asset_bytes = syn::LitByteStr::new(asset_description.as_bytes(), position); 59 | 60 | let section_name = syn::LitStr::new( 61 | manganis_common::linker::LinkSection::CURRENT.link_section, 62 | position, 63 | ); 64 | 65 | quote! { 66 | #[link_section = #section_name] 67 | #[used] 68 | static ASSET: [u8; #len] = * #asset_bytes; 69 | } 70 | } 71 | 72 | /// Collects tailwind classes that will be included in the final binary and returns them unmodified 73 | /// 74 | /// ```rust 75 | /// // You can include tailwind classes that will be collected into the final binary 76 | /// const TAILWIND_CLASSES: &str = manganis::classes!("flex flex-col p-5"); 77 | /// assert_eq!(TAILWIND_CLASSES, "flex flex-col p-5"); 78 | /// ``` 79 | #[proc_macro] 80 | pub fn classes(input: TokenStream) -> TokenStream { 81 | trace_to_file(); 82 | 83 | let input_as_str = parse_macro_input!(input as LitStr); 84 | let input_as_str = input_as_str.value(); 85 | 86 | let asset = manganis_common::AssetType::Tailwind(TailwindAsset::new(&input_as_str)); 87 | 88 | let link_section = generate_link_section(asset); 89 | 90 | quote! { 91 | { 92 | #link_section 93 | #input_as_str 94 | } 95 | } 96 | .into_token_stream() 97 | .into() 98 | } 99 | 100 | /// The mg macro collects assets that will be included in the final binary 101 | /// 102 | /// # Files 103 | /// 104 | /// The file builder collects an arbitrary file. Relative paths are resolved relative to the package root 105 | /// ```rust 106 | /// const _: &str = manganis::mg!("src/asset.txt"); 107 | /// ``` 108 | /// Or you can use URLs to read the asset at build time from a remote location 109 | /// ```rust 110 | /// const _: &str = manganis::mg!("https://rustacean.net/assets/rustacean-flat-happy.png"); 111 | /// ``` 112 | /// 113 | /// # Images 114 | /// 115 | /// You can collect images which will be automatically optimized with the image builder: 116 | /// ```rust 117 | /// const _: manganis::ImageAsset = manganis::mg!(image("rustacean-flat-gesture.png")); 118 | /// ``` 119 | /// Resize the image at compile time to make the assets file size smaller: 120 | /// ```rust 121 | /// const _: manganis::ImageAsset = manganis::mg!(image("rustacean-flat-gesture.png").size(52, 52)); 122 | /// ``` 123 | /// Or convert the image at compile time to a web friendly format: 124 | /// ```rust 125 | /// const _: manganis::ImageAsset = manganis::mg!(image("rustacean-flat-gesture.png").format(ImageFormat::Avif).size(52, 52)); 126 | /// ``` 127 | /// You can mark images as preloaded to make them load faster in your app 128 | /// ```rust 129 | /// const _: manganis::ImageAsset = manganis::mg!(image("rustacean-flat-gesture.png").preload()); 130 | /// ``` 131 | /// 132 | /// # Fonts 133 | /// 134 | /// You can use the font builder to collect fonts that will be included in the final binary from google fonts 135 | /// ```rust 136 | /// const _: &str = manganis::mg!(font().families(["Roboto"])); 137 | /// ``` 138 | /// You can specify weights for the fonts 139 | /// ```rust 140 | /// const _: &str = manganis::mg!(font().families(["Roboto"]).weights([200])); 141 | /// ``` 142 | /// Or set the text to only include the characters you need 143 | /// ```rust 144 | /// const _: &str = manganis::mg!(font().families(["Roboto"]).weights([200]).text("Hello, world!")); 145 | /// ``` 146 | #[proc_macro] 147 | pub fn mg(input: TokenStream) -> TokenStream { 148 | trace_to_file(); 149 | 150 | let asset = parse_macro_input!(input as AnyAssetParser); 151 | 152 | quote! { 153 | #asset 154 | } 155 | .into_token_stream() 156 | .into() 157 | } 158 | 159 | #[derive(Copy, Clone, Default, PartialEq)] 160 | enum ReturnType { 161 | #[default] 162 | AssetSpecific, 163 | StaticStr, 164 | } 165 | 166 | struct AnyAssetParser { 167 | return_type: ReturnType, 168 | asset_type: syn::Result, 169 | source: TokenStream2, 170 | } 171 | 172 | impl ToTokens for AnyAssetParser { 173 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 174 | let asset = match &self.asset_type { 175 | Ok(AnyAssetParserType::File(file)) => file.into_token_stream(), 176 | Ok(AnyAssetParserType::Folder(folder)) => folder.into_token_stream(), 177 | Ok(AnyAssetParserType::Image(image)) => { 178 | let tokens = image.into_token_stream(); 179 | if self.return_type == ReturnType::StaticStr { 180 | quote! { 181 | #tokens.path() 182 | } 183 | } else { 184 | tokens 185 | } 186 | } 187 | Ok(AnyAssetParserType::Font(font)) => font.into_token_stream(), 188 | Ok(AnyAssetParserType::Css(css)) => css.into_token_stream(), 189 | Ok(AnyAssetParserType::Js(js)) => js.into_token_stream(), 190 | Ok(AnyAssetParserType::Json(js)) => js.into_token_stream(), 191 | Err(e) => e.to_compile_error(), 192 | }; 193 | let source = &self.source; 194 | let source = quote! { 195 | const _: &dyn manganis::ForMgMacro = { 196 | use manganis::*; 197 | &#source 198 | }; 199 | }; 200 | 201 | tokens.extend(quote! { 202 | { 203 | #source 204 | #asset 205 | } 206 | }) 207 | } 208 | } 209 | 210 | impl Parse for AnyAssetParser { 211 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 212 | // First try to parse `"myfile".option1().option2()`. We parse that like asset_type("myfile.png").option1().option2() 213 | if input.peek(syn::LitStr) { 214 | let path_str = input.parse::()?; 215 | // Try to parse an extension 216 | let asset = AssetSource::parse_any(&path_str.value()) 217 | .map_err(|e| syn::Error::new(proc_macro2::Span::call_site(), e))?; 218 | let input: proc_macro2::TokenStream = input.parse()?; 219 | let parse_asset = || -> syn::Result { 220 | if let Some(extension) = asset.extension() { 221 | if extension.parse::().is_ok() { 222 | return syn::parse2( 223 | quote_spanned! { path_str.span() => image(#path_str) #input }, 224 | ); 225 | } else if extension.parse::().is_ok() { 226 | return syn::parse2( 227 | quote_spanned! { path_str.span() => video(#path_str) #input }, 228 | ); 229 | } 230 | } 231 | if let AssetSource::Local(path) = &asset { 232 | if path.is_dir() { 233 | return syn::parse2( 234 | quote_spanned! { path_str.span() => folder(#path_str) #input }, 235 | ); 236 | } 237 | } 238 | syn::parse2(quote_spanned! { path_str.span() => file(#path_str) #input }) 239 | }; 240 | 241 | let mut asset = parse_asset()?; 242 | // We always return a static string if the asset was not parsed with an explicit type 243 | asset.return_type = ReturnType::StaticStr; 244 | return Ok(asset); 245 | } 246 | 247 | let builder_tokens = { input.fork().parse::()? }; 248 | 249 | let asset = input.parse::(); 250 | Ok(AnyAssetParser { 251 | return_type: ReturnType::AssetSpecific, 252 | asset_type: asset, 253 | source: builder_tokens, 254 | }) 255 | } 256 | } 257 | 258 | enum AnyAssetParserType { 259 | File(FileAssetParser), 260 | Folder(FolderAssetParser), 261 | Image(ImageAssetParser), 262 | Font(FontAssetParser), 263 | Css(CssAssetParser), 264 | Js(JsAssetParser), 265 | Json(JsonAssetParser), 266 | } 267 | 268 | impl Parse for AnyAssetParserType { 269 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 270 | let ident = input.parse::()?; 271 | let as_string = ident.to_string(); 272 | 273 | Ok(match &*as_string { 274 | // videos and files don't have any special settings yet, we just parse them as files 275 | "video" | "file" => Self::File(input.parse::()?), 276 | "folder" => Self::Folder(input.parse::()?), 277 | "image" => Self::Image(input.parse::()?), 278 | "font" => Self::Font(input.parse::()?), 279 | "css" => Self::Css(input.parse::()?), 280 | "js" => Self::Js(input.parse::()?), 281 | "json" => Self::Json(input.parse::()?), 282 | _ => { 283 | return Err(syn::Error::new( 284 | proc_macro2::Span::call_site(), 285 | format!( 286 | "Unknown asset type: {as_string}. Supported types are file, image, font, and css" 287 | ), 288 | )) 289 | } 290 | }) 291 | } 292 | } 293 | 294 | struct MetadataValue { 295 | key: String, 296 | value: String, 297 | } 298 | 299 | impl Parse for MetadataValue { 300 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 301 | let key = input.parse::()?.to_string(); 302 | input.parse::()?; 303 | let value = input.parse::()?.value(); 304 | Ok(Self { key, value }) 305 | } 306 | } 307 | 308 | /// // You can also collect arbitrary key-value pairs. The meaning of these pairs is determined by the CLI that processes your assets 309 | /// ```rust 310 | /// const _: () = manganis::meta!("opt-level": "3"); 311 | /// ``` 312 | #[proc_macro] 313 | pub fn meta(input: TokenStream) -> TokenStream { 314 | trace_to_file(); 315 | 316 | let md = parse_macro_input!(input as MetadataValue); 317 | 318 | let asset = manganis_common::AssetType::Metadata(MetadataAsset::new( 319 | md.key.as_str(), 320 | md.value.as_str(), 321 | )); 322 | 323 | let link_section = generate_link_section(asset); 324 | 325 | quote! { 326 | { 327 | #link_section 328 | } 329 | } 330 | .into_token_stream() 331 | .into() 332 | } 333 | 334 | fn quote_path(path: &Result) -> TokenStream2 { 335 | match path { 336 | Ok(path) => quote! { #path }, 337 | Err(err) => { 338 | // Expand the error into a warning and return an empty path. Manganis should try not fail to compile the application because it may be checked in CI where manganis CLI support is not available. 339 | let err = err.to_string(); 340 | quote! { 341 | { 342 | #[deprecated(note = #err)] 343 | struct ManganisSupportError; 344 | _ = ManganisSupportError; 345 | "" 346 | } 347 | } 348 | } 349 | } 350 | } 351 | 352 | #[cfg(feature = "url-encoding")] 353 | pub(crate) fn url_encoded_asset( 354 | file_asset: &manganis_common::FileAsset, 355 | ) -> Result { 356 | use base64::Engine; 357 | 358 | let target_directory = 359 | std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string()); 360 | let output_folder = std::path::Path::new(&target_directory) 361 | .join("manganis") 362 | .join("assets"); 363 | std::fs::create_dir_all(&output_folder).map_err(|e| { 364 | syn::Error::new( 365 | proc_macro2::Span::call_site(), 366 | format!("Failed to create output folder: {}", e), 367 | ) 368 | })?; 369 | manganis_cli_support::process_file(file_asset, &output_folder).map_err(|e| { 370 | syn::Error::new( 371 | proc_macro2::Span::call_site(), 372 | format!("Failed to process file: {}", e), 373 | ) 374 | })?; 375 | let file = output_folder.join(file_asset.location().unique_name()); 376 | let data = std::fs::read(file).map_err(|e| { 377 | syn::Error::new( 378 | proc_macro2::Span::call_site(), 379 | format!("Failed to read file: {}", e), 380 | ) 381 | })?; 382 | let data = base64::engine::general_purpose::STANDARD_NO_PAD.encode(data); 383 | let mime = manganis_common::get_mime_from_ext(file_asset.options().extension()); 384 | Ok(format!("data:{mime};base64,{data}")) 385 | } 386 | 387 | pub(crate) fn verify_preload_valid(ident: &Ident) -> Result<(), syn::Error> { 388 | // Compile time preload is only supported for the primary package 389 | if std::env::var("CARGO_PRIMARY_PACKAGE").is_err() { 390 | return Err(syn::Error::new( 391 | ident.span(), 392 | "The `preload` option is only supported for the primary package. Libraries should not preload assets or should preload assets\ 393 | at runtime with utilities your framework provides", 394 | )); 395 | } 396 | 397 | Ok(()) 398 | } 399 | -------------------------------------------------------------------------------- /common/src/file.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::{fmt::Display, str::FromStr}; 3 | 4 | /// The options for a file asset 5 | #[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Clone, Hash)] 6 | pub enum FileOptions { 7 | /// An image asset 8 | Image(ImageOptions), 9 | /// A video asset 10 | Video(VideoOptions), 11 | /// A font asset 12 | Font(FontOptions), 13 | /// A css asset 14 | Css(CssOptions), 15 | /// A JavaScript asset 16 | Js(JsOptions), 17 | /// A Json asset 18 | Json(JsonOptions), 19 | /// Any other asset 20 | Other(UnknownFileOptions), 21 | } 22 | 23 | impl Display for FileOptions { 24 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 25 | match self { 26 | Self::Image(options) => write!(f, "{}", options), 27 | Self::Video(options) => write!(f, "{}", options), 28 | Self::Font(options) => write!(f, "{}", options), 29 | Self::Css(options) => write!(f, "{}", options), 30 | Self::Js(options) => write!(f, "{}", options), 31 | Self::Json(options) => write!(f, "{}", options), 32 | Self::Other(options) => write!(f, "{}", options), 33 | } 34 | } 35 | } 36 | 37 | impl FileOptions { 38 | /// Returns the default options for a given extension 39 | pub fn default_for_extension(extension: Option<&str>) -> Self { 40 | if let Some(extension) = extension { 41 | if extension == CssOptions::EXTENSION { 42 | return Self::Css(CssOptions::default()); 43 | } else if extension == JsonOptions::EXTENSION { 44 | return Self::Json(JsonOptions::default()); 45 | } else if let Ok(ty) = extension.parse::() { 46 | return Self::Image(ImageOptions::new(ty, None)); 47 | } else if let Ok(ty) = extension.parse::() { 48 | return Self::Video(VideoOptions::new(ty)); 49 | } else if let Ok(ty) = extension.parse::() { 50 | return Self::Font(FontOptions::new(ty)); 51 | } else if let Ok(ty) = extension.parse::() { 52 | return Self::Js(JsOptions::new(ty)); 53 | } 54 | } 55 | Self::Other(UnknownFileOptions { 56 | extension: extension.map(String::from), 57 | }) 58 | } 59 | 60 | /// Returns the extension for this file 61 | pub fn extension(&self) -> Option<&str> { 62 | match self { 63 | Self::Image(options) => Some(options.ty.extension()), 64 | Self::Video(options) => Some(options.ty.extension()), 65 | Self::Font(options) => Some(options.ty.extension()), 66 | Self::Css(_) => Some(CssOptions::EXTENSION), 67 | Self::Js(js) => Some(js.ty.extension()), 68 | Self::Json(_) => Some(JsonOptions::EXTENSION), 69 | Self::Other(extension) => extension.extension.as_deref(), 70 | } 71 | } 72 | } 73 | 74 | impl Default for FileOptions { 75 | fn default() -> Self { 76 | Self::Other(UnknownFileOptions { extension: None }) 77 | } 78 | } 79 | 80 | /// The options for an image asset 81 | #[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Clone, Hash)] 82 | pub struct ImageOptions { 83 | compress: bool, 84 | size: Option<(u32, u32)>, 85 | preload: bool, 86 | ty: ImageType, 87 | } 88 | 89 | impl Display for ImageOptions { 90 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 91 | if let Some((x, y)) = self.size { 92 | write!(f, "{} ({}x{})", self.ty, x, y)?; 93 | } else { 94 | write!(f, "{}", self.ty)?; 95 | } 96 | if self.compress { 97 | write!(f, " (compressed)")?; 98 | } 99 | if self.preload { 100 | write!(f, " (preload)")?; 101 | } 102 | Ok(()) 103 | } 104 | } 105 | 106 | impl ImageOptions { 107 | /// Creates a new image options struct 108 | pub fn new(ty: ImageType, size: Option<(u32, u32)>) -> Self { 109 | Self { 110 | compress: true, 111 | size, 112 | ty, 113 | preload: false, 114 | } 115 | } 116 | 117 | /// Returns whether the image should be preloaded 118 | pub fn preload(&self) -> bool { 119 | self.preload 120 | } 121 | 122 | /// Sets whether the image should be preloaded 123 | pub fn set_preload(&mut self, preload: bool) { 124 | self.preload = preload; 125 | } 126 | 127 | /// Returns the image type 128 | pub fn ty(&self) -> &ImageType { 129 | &self.ty 130 | } 131 | 132 | /// Sets the image type 133 | pub fn set_ty(&mut self, ty: ImageType) { 134 | self.ty = ty; 135 | } 136 | 137 | /// Returns the size of the image 138 | pub fn size(&self) -> Option<(u32, u32)> { 139 | self.size 140 | } 141 | 142 | /// Sets the size of the image 143 | pub fn set_size(&mut self, size: Option<(u32, u32)>) { 144 | self.size = size; 145 | } 146 | 147 | /// Returns whether the image should be compressed 148 | pub fn compress(&self) -> bool { 149 | self.compress 150 | } 151 | 152 | /// Sets whether the image should be compressed 153 | pub fn set_compress(&mut self, compress: bool) { 154 | self.compress = compress; 155 | } 156 | } 157 | 158 | /// The type of an image 159 | #[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Clone, Copy, Hash)] 160 | pub enum ImageType { 161 | /// A png image 162 | Png, 163 | /// A jpg image 164 | Jpg, 165 | /// An avif image 166 | Avif, 167 | /// A webp image 168 | Webp, 169 | } 170 | 171 | impl ImageType { 172 | /// Returns the extension for this image type 173 | pub fn extension(&self) -> &'static str { 174 | match self { 175 | Self::Png => "png", 176 | Self::Jpg => "jpg", 177 | Self::Avif => "avif", 178 | Self::Webp => "webp", 179 | } 180 | } 181 | } 182 | 183 | impl Display for ImageType { 184 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 185 | write!(f, "{}", self.extension()) 186 | } 187 | } 188 | 189 | impl FromStr for ImageType { 190 | type Err = (); 191 | 192 | fn from_str(s: &str) -> Result { 193 | match s { 194 | "png" => Ok(Self::Png), 195 | "jpg" | "jpeg" => Ok(Self::Jpg), 196 | "avif" => Ok(Self::Avif), 197 | "webp" => Ok(Self::Webp), 198 | _ => Err(()), 199 | } 200 | } 201 | } 202 | 203 | /// The options for a video asset 204 | #[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Clone, Hash)] 205 | pub struct VideoOptions { 206 | /// Whether the video should be compressed 207 | compress: bool, 208 | /// Whether the video should be preloaded 209 | preload: bool, 210 | /// The type of the video 211 | ty: VideoType, 212 | } 213 | 214 | impl Display for VideoOptions { 215 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 216 | write!(f, "{}", self.ty)?; 217 | if self.compress { 218 | write!(f, " (compressed)")?; 219 | } 220 | if self.preload { 221 | write!(f, " (preload)")?; 222 | } 223 | Ok(()) 224 | } 225 | } 226 | 227 | impl VideoOptions { 228 | /// Creates a new video options struct 229 | pub fn new(ty: VideoType) -> Self { 230 | Self { 231 | compress: true, 232 | ty, 233 | preload: false, 234 | } 235 | } 236 | 237 | /// Returns the type of the video 238 | pub fn ty(&self) -> &VideoType { 239 | &self.ty 240 | } 241 | 242 | /// Sets the type of the video 243 | pub fn set_ty(&mut self, ty: VideoType) { 244 | self.ty = ty; 245 | } 246 | 247 | /// Returns whether the video should be compressed 248 | pub fn compress(&self) -> bool { 249 | self.compress 250 | } 251 | 252 | /// Sets whether the video should be compressed 253 | pub fn set_compress(&mut self, compress: bool) { 254 | self.compress = compress; 255 | } 256 | 257 | /// Returns whether the video should be preloaded 258 | pub fn preload(&self) -> bool { 259 | self.preload 260 | } 261 | 262 | /// Sets whether the video should be preloaded 263 | pub fn set_preload(&mut self, preload: bool) { 264 | self.preload = preload; 265 | } 266 | } 267 | 268 | /// The type of a video 269 | #[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Clone, Hash)] 270 | pub enum VideoType { 271 | /// An mp4 video 272 | MP4, 273 | /// A webm video 274 | Webm, 275 | /// A gif video 276 | GIF, 277 | } 278 | 279 | impl VideoType { 280 | /// Returns the extension for this video type 281 | pub fn extension(&self) -> &'static str { 282 | match self { 283 | Self::MP4 => "mp4", 284 | Self::Webm => "webm", 285 | Self::GIF => "gif", 286 | } 287 | } 288 | } 289 | 290 | impl Display for VideoType { 291 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 292 | write!(f, "{}", self.extension()) 293 | } 294 | } 295 | 296 | impl FromStr for VideoType { 297 | type Err = (); 298 | 299 | fn from_str(s: &str) -> Result { 300 | match s { 301 | "mp4" => Ok(Self::MP4), 302 | "webm" => Ok(Self::Webm), 303 | "gif" => Ok(Self::GIF), 304 | _ => Err(()), 305 | } 306 | } 307 | } 308 | 309 | /// The options for a font asset 310 | #[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Clone, Hash)] 311 | pub struct FontOptions { 312 | ty: FontType, 313 | } 314 | 315 | impl Display for FontOptions { 316 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 317 | write!(f, "{}", self.ty) 318 | } 319 | } 320 | 321 | impl FontOptions { 322 | /// Creates a new font options struct 323 | pub fn new(ty: FontType) -> Self { 324 | Self { ty } 325 | } 326 | 327 | /// Returns the type of the font 328 | pub fn ty(&self) -> &FontType { 329 | &self.ty 330 | } 331 | } 332 | 333 | /// The type of a font 334 | #[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Clone, Hash)] 335 | pub enum FontType { 336 | /// A ttf (TrueType) font 337 | TTF, 338 | /// A woff (Web Open Font Format) font 339 | WOFF, 340 | /// A woff2 (Web Open Font Format 2) font 341 | WOFF2, 342 | } 343 | 344 | impl FontType { 345 | /// Returns the extension for this font type 346 | pub fn extension(&self) -> &'static str { 347 | match self { 348 | Self::TTF => "ttf", 349 | Self::WOFF => "woff", 350 | Self::WOFF2 => "woff2", 351 | } 352 | } 353 | } 354 | 355 | impl FromStr for FontType { 356 | type Err = (); 357 | 358 | fn from_str(s: &str) -> Result { 359 | match s { 360 | "ttf" => Ok(Self::TTF), 361 | "woff" => Ok(Self::WOFF), 362 | "woff2" => Ok(Self::WOFF2), 363 | _ => Err(()), 364 | } 365 | } 366 | } 367 | 368 | impl Display for FontType { 369 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 370 | match self { 371 | Self::TTF => write!(f, "ttf"), 372 | Self::WOFF => write!(f, "woff"), 373 | Self::WOFF2 => write!(f, "woff2"), 374 | } 375 | } 376 | } 377 | 378 | /// The options for a css asset 379 | #[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Clone, Hash)] 380 | pub struct CssOptions { 381 | minify: bool, 382 | preload: bool, 383 | } 384 | 385 | impl Default for CssOptions { 386 | fn default() -> Self { 387 | Self::new() 388 | } 389 | } 390 | 391 | impl Display for CssOptions { 392 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 393 | if self.minify { 394 | write!(f, "minified")?; 395 | } 396 | if self.preload { 397 | write!(f, " (preload)")?; 398 | } 399 | Ok(()) 400 | } 401 | } 402 | 403 | impl CssOptions { 404 | const EXTENSION: &'static str = "css"; 405 | 406 | /// Creates a new css options struct 407 | pub const fn new() -> Self { 408 | Self { 409 | minify: true, 410 | preload: false, 411 | } 412 | } 413 | 414 | /// Returns whether the css should be minified 415 | pub fn minify(&self) -> bool { 416 | self.minify 417 | } 418 | 419 | /// Sets whether the css should be minified 420 | pub fn set_minify(&mut self, minify: bool) { 421 | self.minify = minify; 422 | } 423 | 424 | /// Returns whether the css should be preloaded 425 | pub fn preload(&self) -> bool { 426 | self.preload 427 | } 428 | 429 | /// Sets whether the css should be preloaded 430 | pub fn set_preload(&mut self, preload: bool) { 431 | self.preload = preload; 432 | } 433 | } 434 | 435 | /// The type of a Javascript asset 436 | #[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Clone, Copy, Hash, Default)] 437 | pub enum JsType { 438 | /// A js asset 439 | #[default] 440 | Js, 441 | // TODO: support ts files 442 | } 443 | 444 | impl JsType { 445 | /// Returns the extension for this js type 446 | pub fn extension(&self) -> &'static str { 447 | match self { 448 | Self::Js => "js", 449 | } 450 | } 451 | } 452 | 453 | impl FromStr for JsType { 454 | type Err = (); 455 | 456 | fn from_str(s: &str) -> Result { 457 | match s { 458 | "js" => Ok(Self::Js), 459 | _ => Err(()), 460 | } 461 | } 462 | } 463 | 464 | impl Display for JsType { 465 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 466 | write!(f, "{}", self.extension()) 467 | } 468 | } 469 | 470 | /// The options for a Javascript asset 471 | #[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Clone, Hash, Default)] 472 | pub struct JsOptions { 473 | ty: JsType, 474 | minify: bool, 475 | preload: bool, 476 | } 477 | 478 | impl Display for JsOptions { 479 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 480 | write!(f, "js")?; 481 | Ok(()) 482 | } 483 | } 484 | 485 | impl JsOptions { 486 | /// Creates a new js options struct 487 | pub fn new(ty: JsType) -> Self { 488 | Self { 489 | ty, 490 | preload: false, 491 | minify: true, 492 | } 493 | } 494 | 495 | /// Returns whether the js should be preloaded 496 | pub fn preload(&self) -> bool { 497 | self.preload 498 | } 499 | 500 | /// Sets whether the js should be preloaded 501 | pub fn set_preload(&mut self, preload: bool) { 502 | self.preload = preload; 503 | } 504 | 505 | /// Returns if the js should be minified 506 | pub fn minify(&self) -> bool { 507 | self.minify 508 | } 509 | 510 | /// Sets if the js should be minified 511 | pub fn set_minify(&mut self, minify: bool) { 512 | self.minify = minify; 513 | } 514 | } 515 | 516 | /// The options for a Json asset 517 | #[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Clone, Hash, Default)] 518 | pub struct JsonOptions { 519 | preload: bool, 520 | } 521 | 522 | impl Display for JsonOptions { 523 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 524 | write!(f, "json")?; 525 | Ok(()) 526 | } 527 | } 528 | 529 | impl JsonOptions { 530 | /// The extension of the json asset 531 | pub const EXTENSION: &'static str = "json"; 532 | 533 | /// Creates a new json options struct 534 | pub fn new() -> Self { 535 | Self { preload: false } 536 | } 537 | 538 | /// Returns whether the json should be preloaded 539 | pub fn preload(&self) -> bool { 540 | self.preload 541 | } 542 | 543 | /// Sets whether the json should be preloaded 544 | pub fn set_preload(&mut self, preload: bool) { 545 | self.preload = preload; 546 | } 547 | } 548 | 549 | /// The options for an unknown file asset 550 | #[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Clone, Hash)] 551 | pub struct UnknownFileOptions { 552 | extension: Option, 553 | } 554 | 555 | impl Display for UnknownFileOptions { 556 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 557 | if let Some(extension) = &self.extension { 558 | write!(f, "{}", extension)?; 559 | } 560 | Ok(()) 561 | } 562 | } 563 | 564 | impl UnknownFileOptions { 565 | /// Creates a new unknown file options struct 566 | pub fn new(extension: Option) -> Self { 567 | Self { extension } 568 | } 569 | 570 | /// Returns the extension of the file 571 | pub fn extension(&self) -> Option<&str> { 572 | self.extension.as_deref() 573 | } 574 | } 575 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![deny(missing_docs)] 3 | 4 | #[cfg(feature = "macro")] 5 | pub use manganis_macro::*; 6 | 7 | /// An image asset that is built by the [`mg!`] macro 8 | #[derive(Debug, PartialEq, PartialOrd, Clone, Hash)] 9 | pub struct ImageAsset { 10 | /// The path to the image 11 | path: &'static str, 12 | /// A low quality preview of the image that is URL encoded 13 | preview: Option<&'static str>, 14 | /// A caption for the image 15 | caption: Option<&'static str>, 16 | } 17 | 18 | impl ImageAsset { 19 | /// Creates a new image asset 20 | pub const fn new(path: &'static str) -> Self { 21 | Self { 22 | path, 23 | preview: None, 24 | caption: None, 25 | } 26 | } 27 | 28 | /// Returns the path to the image 29 | pub const fn path(&self) -> &'static str { 30 | self.path 31 | } 32 | 33 | /// Returns the preview of the image 34 | pub const fn preview(&self) -> Option<&'static str> { 35 | self.preview 36 | } 37 | 38 | /// Sets the preview of the image 39 | pub const fn with_preview(self, preview: Option<&'static str>) -> Self { 40 | Self { preview, ..self } 41 | } 42 | 43 | /// Returns the caption of the image 44 | pub const fn caption(&self) -> Option<&'static str> { 45 | self.caption 46 | } 47 | 48 | /// Sets the caption of the image 49 | pub const fn with_caption(self, caption: Option<&'static str>) -> Self { 50 | Self { caption, ..self } 51 | } 52 | } 53 | 54 | impl std::ops::Deref for ImageAsset { 55 | type Target = str; 56 | 57 | fn deref(&self) -> &Self::Target { 58 | self.path 59 | } 60 | } 61 | 62 | impl std::fmt::Display for ImageAsset { 63 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 64 | self.path.fmt(f) 65 | } 66 | } 67 | 68 | /// The type of an image. You can read more about the tradeoffs between image formats [here](https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types) 69 | #[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Hash)] 70 | pub enum ImageType { 71 | /// A png image. Png images cannot contain transparency and tend to compress worse than other formats 72 | Png, 73 | /// A jpg image. Jpg images can contain transparency and tend to compress better than png images 74 | Jpg, 75 | /// A webp image. Webp images can contain transparency and tend to compress better than jpg images 76 | Webp, 77 | /// An avif image. Avif images can compress slightly better than webp images but are not supported by all browsers 78 | Avif, 79 | } 80 | 81 | /// A builder for an image asset. This must be used in the [`mg!`] macro. 82 | /// 83 | /// > **Note**: This will do nothing outside of the `mg!` macro 84 | pub struct ImageAssetBuilder; 85 | 86 | impl ImageAssetBuilder { 87 | /// Sets the format of the image 88 | /// 89 | /// > **Note**: This will do nothing outside of the `mg!` macro 90 | /// 91 | /// Choosing the right format can make your site load much faster. Webp and avif images tend to be a good default for most images 92 | /// 93 | /// ```rust 94 | /// const _: manganis::ImageAsset = manganis::mg!(image("https://avatars.githubusercontent.com/u/79236386?s=48&v=4").format(ImageType::Webp)); 95 | /// ``` 96 | #[allow(unused)] 97 | pub const fn format(self, format: ImageType) -> Self { 98 | Self 99 | } 100 | 101 | /// Sets the size of the image 102 | /// 103 | /// > **Note**: This will do nothing outside of the `mg!` macro 104 | /// 105 | /// If you only use the image in one place, you can set the size of the image to the size it will be displayed at. This will make the image load faster 106 | /// 107 | /// ```rust 108 | /// const _: manganis::ImageAsset = manganis::mg!(image("https://avatars.githubusercontent.com/u/79236386?s=48&v=4").size(512, 512)); 109 | /// ``` 110 | #[allow(unused)] 111 | pub const fn size(self, x: u32, y: u32) -> Self { 112 | Self 113 | } 114 | 115 | /// Make the image use a low quality preview 116 | /// 117 | /// > **Note**: This will do nothing outside of the `mg!` macro 118 | /// 119 | /// A low quality preview is a small version of the image that will load faster. This is useful for large images on mobile devices that may take longer to load 120 | /// 121 | /// ```rust 122 | /// const _: manganis::ImageAsset = manganis::mg!(image("https://avatars.githubusercontent.com/u/79236386?s=48&v=4").low_quality_preview()); 123 | /// ``` 124 | #[allow(unused)] 125 | pub const fn low_quality_preview(self) -> Self { 126 | Self 127 | } 128 | 129 | /// Make the image preloaded 130 | /// 131 | /// > **Note**: This will do nothing outside of the `mg!` macro 132 | /// 133 | /// Preloading an image will make the image start to load as soon as possible. This is useful for images that will be displayed soon after the page loads or images that may not be visible immediately, but should start loading sooner 134 | /// 135 | /// ```rust 136 | /// const _: manganis::ImageAsset = manganis::mg!(image("https://avatars.githubusercontent.com/u/79236386?s=48&v=4").preload()); 137 | /// ``` 138 | #[allow(unused)] 139 | pub const fn preload(self) -> Self { 140 | Self 141 | } 142 | 143 | /// Make the image URL encoded 144 | /// 145 | /// > **Note**: This will do nothing outside of the `mg!` macro 146 | /// 147 | /// URL encoding an image inlines the data of the image into the URL. This is useful for small images that should load as soon as the html is parsed 148 | /// 149 | /// ```rust 150 | /// const _: manganis::ImageAsset = manganis::mg!(image("https://avatars.githubusercontent.com/u/79236386?s=48&v=4").url_encoded()); 151 | /// ``` 152 | #[allow(unused)] 153 | pub const fn url_encoded(self) -> Self { 154 | Self 155 | } 156 | } 157 | 158 | /// Create an image asset from the local path or url to the image 159 | /// 160 | /// > **Note**: This will do nothing outside of the `mg!` macro 161 | /// 162 | /// You can collect images which will be automatically optimized with the image builder: 163 | /// ```rust 164 | /// const _: manganis::ImageAsset = manganis::mg!(image("rustacean-flat-gesture.png")); 165 | /// ``` 166 | /// Resize the image at compile time to make the assets file size smaller: 167 | /// ```rust 168 | /// const _: manganis::ImageAsset = manganis::mg!(image("rustacean-flat-gesture.png").size(52, 52)); 169 | /// ``` 170 | /// Or convert the image at compile time to a web friendly format: 171 | /// ```rust 172 | /// const _: manganis::ImageAsset = manganis::mg!(image("rustacean-flat-gesture.png").format(ImageType::Avif).size(52, 52)); 173 | /// ``` 174 | /// You can mark images as preloaded to make them load faster in your app 175 | /// ```rust 176 | /// const _: manganis::ImageAsset = manganis::mg!(image("rustacean-flat-gesture.png").preload()); 177 | /// ``` 178 | #[allow(unused)] 179 | pub const fn image(path: &'static str) -> ImageAssetBuilder { 180 | ImageAssetBuilder 181 | } 182 | 183 | /// A builder for a css asset. This must be used in the [`mg!`] macro. 184 | /// 185 | /// > **Note**: This will do nothing outside of the `mg!` macro 186 | pub struct CssAssetBuilder; 187 | 188 | impl CssAssetBuilder { 189 | /// Sets whether the css should be minified (default: true) 190 | /// 191 | /// > **Note**: This will do nothing outside of the `mg!` macro 192 | /// 193 | /// Minifying the css can make your site load faster by loading less data 194 | /// 195 | /// ```rust 196 | /// const _: &str = manganis::mg!(css("https://sindresorhus.com/github-markdown-css/github-markdown.css").minify(false)); 197 | /// ``` 198 | #[allow(unused)] 199 | pub const fn minify(self, minify: bool) -> Self { 200 | Self 201 | } 202 | 203 | /// Make the css preloaded 204 | /// 205 | /// > **Note**: This will do nothing outside of the `mg!` macro 206 | /// 207 | /// Preloading css will make the css start to load as soon as possible. This is useful for css that will be displayed soon after the page loads or css that may not be visible immediately, but should start loading sooner 208 | /// 209 | /// ```rust 210 | /// const _: &str = manganis::mg!(css("https://sindresorhus.com/github-markdown-css/github-markdown.css").preload()); 211 | /// ``` 212 | #[allow(unused)] 213 | pub const fn preload(self) -> Self { 214 | Self 215 | } 216 | 217 | /// Make the css URL encoded 218 | /// 219 | /// > **Note**: This will do nothing outside of the `mg!` macro 220 | /// 221 | /// URL encoding an image inlines the data of the css into the URL. This is useful for small css files that should load as soon as the html is parsed 222 | /// 223 | /// ```rust 224 | /// const _: &str = manganis::mg!(css("https://sindresorhus.com/github-markdown-css/github-markdown.css").url_encoded()); 225 | /// ``` 226 | #[allow(unused)] 227 | pub const fn url_encoded(self) -> Self { 228 | Self 229 | } 230 | } 231 | 232 | /// A builder for a javascript asset. This must be used in the [`mg!`] macro. 233 | /// 234 | /// > **Note**: This will do nothing outside of the `mg!` macro 235 | pub struct JsAssetBuilder; 236 | 237 | impl JsAssetBuilder { 238 | /// Sets whether the js should be minified (default: true) 239 | /// 240 | /// > **Note**: This will do nothing outside of the `mg!` macro 241 | /// 242 | /// Minifying the js can make your site load faster by loading less data 243 | /// 244 | /// ```rust 245 | /// const _: &str = manganis::mg!(js("assets/script.js").minify(false)); 246 | /// ``` 247 | #[allow(unused)] 248 | pub const fn minify(self, minify: bool) -> Self { 249 | Self 250 | } 251 | 252 | /// Make the js preloaded 253 | /// 254 | /// > **Note**: This will do nothing outside of the `mg!` macro 255 | /// 256 | /// Preloading js will make the js start to load as soon as possible. This is useful for js that will be run soon after the page loads or js that may not be used immediately, but should start loading sooner 257 | /// 258 | /// ```rust 259 | /// const _: &str = manganis::mg!(js("assets/script.js").preload()); 260 | /// ``` 261 | #[allow(unused)] 262 | pub const fn preload(self) -> Self { 263 | Self 264 | } 265 | 266 | /// Make the js URL encoded 267 | /// 268 | /// > **Note**: This will do nothing outside of the `mg!` macro 269 | /// 270 | /// URL encoding an image inlines the data of the js into the URL. This is useful for small js files that should load as soon as the html is parsed 271 | /// 272 | /// ```rust 273 | /// const _: &str = manganis::mg!(js("assets/script.js").url_encoded()); 274 | /// ``` 275 | #[allow(unused)] 276 | pub const fn url_encoded(self) -> Self { 277 | Self 278 | } 279 | } 280 | 281 | /// A builder for a json asset. This must be used in the [`mg!`] macro. 282 | /// 283 | /// > **Note**: This will do nothing outside of the `mg!` macro 284 | pub struct JsonAssetBuilder; 285 | 286 | impl JsonAssetBuilder { 287 | /// Make the json preloaded 288 | /// 289 | /// > **Note**: This will do nothing outside of the `mg!` macro 290 | /// 291 | /// Preloading json will make the json start to load as soon as possible. This is useful for json that will be run soon after the page loads or json that may not be used immediately, but should start loading sooner 292 | /// 293 | /// ```rust 294 | /// const _: &str = manganis::mg!(json("assets/data.json").preload()); 295 | /// ``` 296 | #[allow(unused)] 297 | pub const fn preload(self) -> Self { 298 | Self 299 | } 300 | 301 | /// Make the json URL encoded 302 | /// 303 | /// > **Note**: This will do nothing outside of the `mg!` macro 304 | /// 305 | /// URL encoding an image inlines the data of the json into the URL. This is useful for small json files that should load as soon as the html is parsed 306 | /// 307 | /// ```rust 308 | /// const _: &str = manganis::mg!(json("assets/data.json").url_encoded()); 309 | /// ``` 310 | #[allow(unused)] 311 | pub const fn url_encoded(self) -> Self { 312 | Self 313 | } 314 | } 315 | 316 | /// Create an css asset from the local path or url to the css 317 | /// 318 | /// > **Note**: This will do nothing outside of the `mg!` macro 319 | /// 320 | /// You can collect css which will be automatically minified with the css builder: 321 | /// ```rust 322 | /// const _: &str = manganis::mg!(css("https://sindresorhus.com/github-markdown-css/github-markdown.css")); 323 | /// ``` 324 | /// You can mark css as preloaded to make them load faster in your app: 325 | /// ```rust 326 | /// const _: &str = manganis::mg!(css("https://sindresorhus.com/github-markdown-css/github-markdown.css").preload()); 327 | /// ``` 328 | #[allow(unused)] 329 | pub const fn css(path: &'static str) -> CssAssetBuilder { 330 | CssAssetBuilder 331 | } 332 | 333 | /// A builder for a font asset. This must be used in the `mg!` macro. 334 | /// 335 | /// > **Note**: This will do nothing outside of the `mg!` macro 336 | pub struct FontAssetBuilder; 337 | 338 | impl FontAssetBuilder { 339 | /// Sets the font family of the font 340 | /// 341 | /// > **Note**: This will do nothing outside of the `mg!` macro 342 | /// 343 | /// ```rust 344 | /// const _: &str = manganis::mg!(font().families(["Roboto"])); 345 | /// ``` 346 | #[allow(unused)] 347 | pub const fn families(self, families: [&'static str; N]) -> Self { 348 | Self 349 | } 350 | 351 | /// Sets the font weight of the font 352 | /// 353 | /// > **Note**: This will do nothing outside of the `mg!` macro 354 | /// 355 | /// ```rust 356 | /// const _: &str = manganis::mg!(font().families(["Roboto"]).weights([200])); 357 | /// ``` 358 | #[allow(unused)] 359 | pub const fn weights(self, weights: [u32; N]) -> Self { 360 | Self 361 | } 362 | 363 | /// Sets the subset of text that the font needs to support. The font will only include the characters in the text which can make the font file size significantly smaller 364 | /// 365 | /// > **Note**: This will do nothing outside of the `mg!` macro 366 | /// 367 | /// ```rust 368 | /// const _: &str = manganis::mg!(font().families(["Roboto"]).weights([200]).text("Hello, world!")); 369 | /// ``` 370 | #[allow(unused)] 371 | pub const fn text(self, text: &'static str) -> Self { 372 | Self 373 | } 374 | 375 | /// Sets the [display](https://www.w3.org/TR/css-fonts-4/#font-display-desc) of the font. The display control what happens when the font is unavailable 376 | /// 377 | /// > **Note**: This will do nothing outside of the `mg!` macro 378 | /// 379 | /// ```rust 380 | /// const _: &str = manganis::mg!(font().families(["Roboto"]).weights([200]).text("Hello, world!").display("swap")); 381 | /// ``` 382 | #[allow(unused)] 383 | pub const fn display(self, display: &'static str) -> Self { 384 | Self 385 | } 386 | } 387 | 388 | /// Create a font asset 389 | /// 390 | /// > **Note**: This will do nothing outside of the `mg!` macro 391 | /// 392 | /// You can use the font builder to collect fonts that will be included in the final binary from google fonts 393 | /// ```rust 394 | /// const _: &str = manganis::mg!(font().families(["Roboto"])); 395 | /// ``` 396 | /// You can specify weights for the fonts 397 | /// ```rust 398 | /// const _: &str = manganis::mg!(font().families(["Roboto"]).weights([200])); 399 | /// ``` 400 | /// Or set the text to only include the characters you need 401 | /// ```rust 402 | /// const _: &str = manganis::mg!(font().families(["Roboto"]).weights([200]).text("Hello, world!")); 403 | /// ``` 404 | #[allow(unused)] 405 | pub const fn font() -> FontAssetBuilder { 406 | FontAssetBuilder 407 | } 408 | 409 | /// Create an file asset from the local path or url to the file 410 | /// 411 | /// > **Note**: This will do nothing outside of the `mg!` macro 412 | /// 413 | /// The file builder collects an arbitrary file. Relative paths are resolved relative to the package root 414 | /// ```rust 415 | /// const _: &str = manganis::mg!("/assets/asset.txt"); 416 | /// ``` 417 | /// Or you can use URLs to read the asset at build time from a remote location 418 | /// ```rust 419 | /// const _: &str = manganis::mg!("https://rustacean.net/assets/rustacean-flat-happy.png"); 420 | /// ``` 421 | #[allow(unused)] 422 | pub const fn file(path: &'static str) -> &'static str { 423 | path 424 | } 425 | 426 | /// Create a video asset from the local path or url to the video 427 | /// 428 | /// > **Note**: This will do nothing outside of the `mg!` macro 429 | /// 430 | /// The video builder collects an arbitrary file. Relative paths are resolved relative to the package root 431 | /// ```rust 432 | /// const _: &str = manganis::mg!(video("/assets/video.mp4")); 433 | /// ``` 434 | /// Or you can use URLs to read the asset at build time from a remote location 435 | /// ```rust 436 | /// const _: &str = manganis::mg!(video("https://private-user-images.githubusercontent.com/66571940/355646745-10781eef-de07-491d-aaa3-f75949b32190.mov?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjMxMzI5NTcsIm5iZiI6MTcyMzEzMjY1NywicGF0aCI6Ii82NjU3MTk0MC8zNTU2NDY3NDUtMTA3ODFlZWYtZGUwNy00OTFkLWFhYTMtZjc1OTQ5YjMyMTkwLm1vdj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA4MDglMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwODA4VDE1NTczN1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTVkODEwZjI4ODE2ZmM4MjE3MWQ2ZDk3MjQ0NjQxYmZlMDI2OTAyMzhjNGU4MzlkYTdmZWM1MjI4ZWQ5NDg3M2QmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.jlX5E6WGjZeqZind6UCRLFrJ9NHcsV8xXy-Ls30tKPQ")); 437 | /// ``` 438 | #[allow(unused)] 439 | pub const fn video(path: &'static str) -> &'static str { 440 | path 441 | } 442 | /// Create an folder asset from the local path 443 | /// 444 | /// > **Note**: This will do nothing outside of the `mg!` macro 445 | /// 446 | /// The folder builder collects an arbitrary local folder. Relative paths are resolved relative to the package root 447 | /// ```rust 448 | /// const _: &str = manganis::mg!("/assets"); 449 | /// ``` 450 | #[allow(unused)] 451 | pub const fn folder(path: &'static str) -> &'static str { 452 | path 453 | } 454 | 455 | /// A trait for something that can be used in the `mg!` macro 456 | /// 457 | /// > **Note**: These types will do nothing outside of the `mg!` macro 458 | pub trait ForMgMacro: __private::Sealed + Sync + Send {} 459 | 460 | impl ForMgMacro for T where T: __private::Sealed + Sync + Send {} 461 | 462 | mod __private { 463 | use super::*; 464 | 465 | pub trait Sealed {} 466 | 467 | impl Sealed for ImageAssetBuilder {} 468 | impl Sealed for FontAssetBuilder {} 469 | impl Sealed for JsAssetBuilder {} 470 | impl Sealed for JsonAssetBuilder {} 471 | impl Sealed for CssAssetBuilder {} 472 | impl Sealed for &'static str {} 473 | } 474 | -------------------------------------------------------------------------------- /common/src/asset.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::Display, 3 | hash::{DefaultHasher, Hash, Hasher}, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | use anyhow::Context; 8 | use base64::Engine; 9 | use serde::{Deserialize, Serialize}; 10 | use url::Url; 11 | 12 | use crate::{cache::manifest_dir, Config, FileOptions}; 13 | 14 | /// The maximum length of a path segment 15 | const MAX_PATH_LENGTH: usize = 128; 16 | /// The length of the hash in the output path 17 | const HASH_SIZE: usize = 16; 18 | 19 | /// The type of asset 20 | #[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Clone)] 21 | pub enum AssetType { 22 | /// A file asset 23 | File(FileAsset), 24 | /// A folder asset 25 | Folder(FolderAsset), 26 | /// A tailwind class asset 27 | Tailwind(TailwindAsset), 28 | /// A metadata asset 29 | Metadata(MetadataAsset), 30 | } 31 | 32 | /// The source of a file asset 33 | #[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Clone, Hash, Eq)] 34 | pub enum AssetSource { 35 | /// A local file 36 | Local(PathBuf), 37 | /// A remote file 38 | Remote(Url), 39 | } 40 | 41 | impl Display for AssetSource { 42 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 43 | let as_string = match self { 44 | Self::Local(path) => path.display().to_string(), 45 | Self::Remote(url) => url.as_str().to_string(), 46 | }; 47 | if as_string.len() > 25 { 48 | write!(f, "{}...", &as_string[..25]) 49 | } else { 50 | write!(f, "{}", as_string) 51 | } 52 | } 53 | } 54 | 55 | impl AssetSource { 56 | /// Try to convert the asset source to a path 57 | pub fn as_path(&self) -> Option<&PathBuf> { 58 | match self { 59 | Self::Local(path) => Some(path), 60 | Self::Remote(_) => None, 61 | } 62 | } 63 | 64 | /// Try to convert the asset source to a url 65 | pub fn as_url(&self) -> Option<&Url> { 66 | match self { 67 | Self::Local(_) => None, 68 | Self::Remote(url) => Some(url), 69 | } 70 | } 71 | 72 | /// Returns the last segment of the file source used to generate a unique name 73 | pub fn last_segment(&self) -> &str { 74 | match self { 75 | Self::Local(path) => path.file_name().unwrap().to_str().unwrap(), 76 | Self::Remote(url) => url.path_segments().unwrap().last().unwrap(), 77 | } 78 | } 79 | 80 | /// Returns the extension of the file source 81 | pub fn extension(&self) -> Option { 82 | match self { 83 | Self::Local(path) => path.extension().map(|e| e.to_str().unwrap().to_string()), 84 | Self::Remote(_url) => None, 85 | } 86 | } 87 | 88 | /// Attempts to get the mime type of the file source 89 | pub fn mime_type(&self) -> Option { 90 | match self { 91 | Self::Local(path) => get_mime_from_path(path).ok().map(|mime| mime.to_string()), 92 | Self::Remote(_url) => None, 93 | } 94 | } 95 | 96 | /// Find when the asset was last updated 97 | pub fn last_updated(&self) -> Option { 98 | match self { 99 | Self::Local(path) => path.metadata().ok().and_then(|metadata| { 100 | metadata 101 | .modified() 102 | .ok() 103 | .map(|modified| format!("{:?}", modified)) 104 | .or_else(|| { 105 | metadata 106 | .created() 107 | .ok() 108 | .map(|created| format!("{:?}", created)) 109 | }) 110 | }), 111 | Self::Remote(_url) => None, 112 | } 113 | } 114 | 115 | /// Reads the file to a string 116 | pub fn read_to_string(&self) -> anyhow::Result { 117 | match &self { 118 | AssetSource::Local(path) => Ok(std::fs::read_to_string(path).with_context(|| { 119 | format!("Failed to read file from location: {}", path.display()) 120 | })?), 121 | AssetSource::Remote(_url) => Err(anyhow::anyhow!("no more remote urls!")), 122 | } 123 | } 124 | 125 | /// Reads the file to bytes 126 | pub fn read_to_bytes(&self) -> anyhow::Result> { 127 | match &self { 128 | AssetSource::Local(path) => Ok(std::fs::read(path).with_context(|| { 129 | format!("Failed to read file from location: {}", path.display()) 130 | })?), 131 | AssetSource::Remote(_url) => Err(anyhow::anyhow!("no more remote urls!")), 132 | } 133 | } 134 | } 135 | 136 | /// Get the mime type from a URI using its extension 137 | fn _ext_of_mime(mime: &str) -> &str { 138 | let mime = mime.split(';').next().unwrap_or_default(); 139 | match mime.trim() { 140 | "application/octet-stream" => "bin", 141 | "text/css" => "css", 142 | "text/csv" => "csv", 143 | "text/html" => "html", 144 | "image/vnd.microsoft.icon" => "ico", 145 | "text/javascript" => "js", 146 | "application/json" => "json", 147 | "application/ld+json" => "jsonld", 148 | "application/rtf" => "rtf", 149 | "image/svg+xml" => "svg", 150 | "video/mp4" => "mp4", 151 | "text/plain" => "txt", 152 | "application/xml" => "xml", 153 | "application/zip" => "zip", 154 | "image/png" => "png", 155 | "image/jpeg" => "jpg", 156 | "image/gif" => "gif", 157 | "image/webp" => "webp", 158 | "image/avif" => "avif", 159 | "font/ttf" => "ttf", 160 | "font/woff" => "woff", 161 | "font/woff2" => "woff2", 162 | other => other.split('/').last().unwrap_or_default(), 163 | } 164 | } 165 | 166 | /// Get the mime type from a path-like string 167 | fn get_mime_from_path(trimmed: &Path) -> std::io::Result<&'static str> { 168 | if trimmed.extension().is_some_and(|ext| ext == "svg") { 169 | return Ok("image/svg+xml"); 170 | } 171 | 172 | let res = match infer::get_from_path(trimmed)?.map(|f| f.mime_type()) { 173 | Some(f) => { 174 | if f == "text/plain" { 175 | get_mime_by_ext(trimmed) 176 | } else { 177 | f 178 | } 179 | } 180 | None => get_mime_by_ext(trimmed), 181 | }; 182 | 183 | Ok(res) 184 | } 185 | 186 | /// Get the mime type from a URI using its extension 187 | fn get_mime_by_ext(trimmed: &Path) -> &'static str { 188 | get_mime_from_ext(trimmed.extension().and_then(|e| e.to_str())) 189 | } 190 | 191 | /// Get the mime type from a URI using its extension 192 | pub fn get_mime_from_ext(extension: Option<&str>) -> &'static str { 193 | match extension { 194 | Some("bin") => "application/octet-stream", 195 | Some("css") => "text/css", 196 | Some("csv") => "text/csv", 197 | Some("html") => "text/html", 198 | Some("ico") => "image/vnd.microsoft.icon", 199 | Some("js") => "text/javascript", 200 | Some("json") => "application/json", 201 | Some("jsonld") => "application/ld+json", 202 | Some("mjs") => "text/javascript", 203 | Some("rtf") => "application/rtf", 204 | Some("svg") => "image/svg+xml", 205 | Some("mp4") => "video/mp4", 206 | Some("png") => "image/png", 207 | Some("jpg") => "image/jpeg", 208 | Some("gif") => "image/gif", 209 | Some("webp") => "image/webp", 210 | Some("avif") => "image/avif", 211 | Some("txt") => "text/plain", 212 | // Assume HTML when a TLD is found for eg. `dioxus:://dioxuslabs.app` | `dioxus://hello.com` 213 | Some(_) => "text/html", 214 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types 215 | // using octet stream according to this: 216 | None => "application/octet-stream", 217 | } 218 | } 219 | 220 | /// The location of an asset before and after it is collected 221 | #[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Clone, Hash, Eq)] 222 | pub struct AssetLocation { 223 | unique_name: String, 224 | source: AssetSource, 225 | } 226 | 227 | impl AssetLocation { 228 | /// Returns the unique name of the file that the asset will be served from 229 | pub fn unique_name(&self) -> &str { 230 | &self.unique_name 231 | } 232 | 233 | /// Returns the source of the file that the asset will be collected from 234 | pub fn source(&self) -> &AssetSource { 235 | &self.source 236 | } 237 | } 238 | 239 | /// Error while checking an asset exists 240 | #[derive(Debug)] 241 | pub enum AssetError { 242 | /// The relative path does not exist 243 | NotFoundRelative(PathBuf, String), 244 | /// The path exist but is not a file 245 | NotFile(PathBuf), 246 | /// The path exist but is not a folder 247 | NotFolder(PathBuf), 248 | /// Unknown IO error 249 | IO(PathBuf, std::io::Error), 250 | } 251 | 252 | impl Display for AssetError { 253 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 254 | match self { 255 | AssetError::NotFoundRelative(manifest_dir, path) => 256 | write!(f,"cannot find file `{}` in `{}`, please make sure it exists.\nAny relative paths are resolved relative to the manifest directory.", 257 | path, 258 | manifest_dir.display() 259 | ), 260 | AssetError::NotFile(absolute_path) => 261 | write!(f, "`{}` is not a file, please choose a valid asset.\nAny relative paths are resolved relative to the manifest directory.", absolute_path.display()), 262 | AssetError::NotFolder(absolute_path) => 263 | write!(f, "`{}` is not a folder, please choose a valid asset.\nAny relative paths are resolved relative to the manifest directory.", absolute_path.display()), 264 | AssetError::IO(absolute_path, err) => 265 | write!(f, "unknown error when accessing `{}`: \n{}", absolute_path.display(), err) 266 | } 267 | } 268 | } 269 | 270 | impl AssetSource { 271 | /// Parse a string as a file source 272 | pub fn parse_file(path: &str) -> Result { 273 | let myself = Self::parse_any(path)?; 274 | if let Self::Local(path) = &myself { 275 | if !path.is_file() { 276 | return Err(AssetError::NotFile(path.to_path_buf())); 277 | } 278 | } 279 | Ok(myself) 280 | } 281 | 282 | /// Parse a string as a folder source 283 | pub fn parse_folder(path: &str) -> Result { 284 | let myself = Self::parse_any(path)?; 285 | if let Self::Local(path) = &myself { 286 | if !path.is_dir() { 287 | return Err(AssetError::NotFolder(path.to_path_buf())); 288 | } 289 | } 290 | Ok(myself) 291 | } 292 | 293 | /// Parse a string as a file or folder source 294 | pub fn parse_any(src: &str) -> Result { 295 | match Url::parse(src) { 296 | Ok(url) => Ok(Self::Remote(url)), 297 | Err(_) => { 298 | let manifest_dir = manifest_dir(); 299 | let path = PathBuf::from(src); 300 | // Paths are always relative to the manifest directory. 301 | // If the path is absolute, we need to make it relative to the manifest directory. 302 | let path = path 303 | .strip_prefix(std::path::MAIN_SEPARATOR_STR) 304 | .unwrap_or(&path); 305 | let path = manifest_dir.join(path); 306 | 307 | match path.canonicalize() { 308 | Ok(x) => Ok(Self::Local(x)), 309 | // relative path does not exist 310 | Err(e) if e.kind() == std::io::ErrorKind::NotFound => { 311 | Err(AssetError::NotFoundRelative(manifest_dir, src.into())) 312 | } 313 | // other error 314 | Err(e) => Err(AssetError::IO(path, e)), 315 | } 316 | } 317 | } 318 | } 319 | } 320 | 321 | /// A folder asset 322 | #[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Clone)] 323 | pub struct FolderAsset { 324 | location: AssetLocation, 325 | } 326 | 327 | impl Display for FolderAsset { 328 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 329 | write!(f, "{}/**", self.location.source(),) 330 | } 331 | } 332 | 333 | impl FolderAsset { 334 | /// Creates a new folder asset 335 | pub fn new(source: AssetSource) -> Self { 336 | let AssetSource::Local(source) = source else { 337 | panic!("Folder asset must be a local path"); 338 | }; 339 | assert!(source.is_dir()); 340 | 341 | let mut myself = Self { 342 | location: AssetLocation { 343 | unique_name: Default::default(), 344 | source: AssetSource::Local(source), 345 | }, 346 | }; 347 | 348 | myself.regenerate_unique_name(); 349 | 350 | myself 351 | } 352 | 353 | /// Returns the location where the folder asset will be served from or None if the asset cannot be served 354 | pub fn served_location(&self) -> Result { 355 | resolve_asset_location(&self.location) 356 | } 357 | 358 | /// Returns the unique name of the folder asset 359 | pub fn unique_name(&self) -> &str { 360 | &self.location.unique_name 361 | } 362 | 363 | /// Returns the location of the folder asset 364 | pub fn location(&self) -> &AssetLocation { 365 | &self.location 366 | } 367 | 368 | /// Create a unique hash for the source folder by recursively hashing the files 369 | fn hash(&self) -> u64 { 370 | let mut hash = std::collections::hash_map::DefaultHasher::new(); 371 | let folder = self 372 | .location 373 | .source 374 | .as_path() 375 | .expect("Folder asset must be a local path"); 376 | let mut folders_queued = vec![folder.clone()]; 377 | while let Some(folder) = folders_queued.pop() { 378 | // Add the folder to the hash 379 | for segment in folder.iter() { 380 | segment.hash(&mut hash); 381 | } 382 | 383 | let files = std::fs::read_dir(folder).into_iter().flatten().flatten(); 384 | for file in files { 385 | let path = file.path(); 386 | let metadata = path.metadata().unwrap(); 387 | // If the file is a folder, add it to the queue otherwise add it to the hash 388 | if metadata.is_dir() { 389 | folders_queued.push(path); 390 | } else { 391 | hash_file(&AssetSource::Local(path), &mut hash); 392 | } 393 | } 394 | } 395 | 396 | // Add the manganis version to the hash 397 | hash_version(&mut hash); 398 | 399 | hash.finish() 400 | } 401 | 402 | /// Regenerate the unique name of the folder asset 403 | fn regenerate_unique_name(&mut self) { 404 | let uuid = self.hash(); 405 | let file_name = normalized_file_name(&self.location.source, None); 406 | self.location.unique_name = format!("{file_name}{uuid:x}"); 407 | assert!(self.location.unique_name.len() <= MAX_PATH_LENGTH); 408 | } 409 | } 410 | 411 | /// A file asset 412 | #[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Clone)] 413 | pub struct FileAsset { 414 | location: AssetLocation, 415 | options: FileOptions, 416 | url_encoded: bool, 417 | } 418 | 419 | impl Display for FileAsset { 420 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 421 | let url_encoded = if self.url_encoded { 422 | " [url encoded]" 423 | } else { 424 | "" 425 | }; 426 | write!( 427 | f, 428 | "{} [{}]{}", 429 | self.location.source(), 430 | self.options, 431 | url_encoded 432 | ) 433 | } 434 | } 435 | 436 | impl FileAsset { 437 | /// Creates a new file asset 438 | pub fn new(source: AssetSource) -> Self { 439 | if let Some(path) = source.as_path() { 440 | assert!(!path.is_dir()); 441 | } 442 | 443 | let options = FileOptions::default_for_extension(source.extension().as_deref()); 444 | 445 | let mut myself = Self { 446 | location: AssetLocation { 447 | unique_name: Default::default(), 448 | source, 449 | }, 450 | options, 451 | url_encoded: false, 452 | }; 453 | 454 | myself.regenerate_unique_name(); 455 | 456 | myself 457 | } 458 | 459 | /// Set the file options 460 | pub fn with_options(self, options: FileOptions) -> Self { 461 | let mut myself = Self { 462 | location: self.location, 463 | options, 464 | url_encoded: false, 465 | }; 466 | 467 | myself.regenerate_unique_name(); 468 | 469 | myself 470 | } 471 | 472 | /// Set whether the file asset should be url encoded 473 | pub fn set_url_encoded(&mut self, url_encoded: bool) { 474 | self.url_encoded = url_encoded; 475 | } 476 | 477 | /// Returns whether the file asset should be url encoded 478 | pub fn url_encoded(&self) -> bool { 479 | self.url_encoded 480 | } 481 | 482 | /// Returns the location where the file asset will be served from or None if the asset cannot be served 483 | pub fn served_location(&self) -> Result { 484 | if self.url_encoded { 485 | let data = self.location.source.read_to_bytes().unwrap(); 486 | let data = base64::engine::general_purpose::STANDARD_NO_PAD.encode(data); 487 | let mime = self.location.source.mime_type().unwrap(); 488 | Ok(format!("data:{mime};base64,{data}")) 489 | } else { 490 | resolve_asset_location(&self.location) 491 | } 492 | } 493 | 494 | /// Returns the location of the file asset 495 | pub fn location(&self) -> &AssetLocation { 496 | &self.location 497 | } 498 | 499 | /// Returns the options for the file asset 500 | pub fn options(&self) -> &FileOptions { 501 | &self.options 502 | } 503 | 504 | /// Returns the options for the file asset mutably 505 | pub fn with_options_mut(&mut self, f: impl FnOnce(&mut FileOptions)) { 506 | f(&mut self.options); 507 | self.regenerate_unique_name(); 508 | } 509 | 510 | /// Hash the file asset source and options 511 | fn hash(&self) -> u64 { 512 | let mut hash = std::collections::hash_map::DefaultHasher::new(); 513 | hash_file(&self.location.source, &mut hash); 514 | self.options.hash(&mut hash); 515 | hash_version(&mut hash); 516 | hash.finish() 517 | } 518 | 519 | /// Regenerates the unique name of the file asset 520 | fn regenerate_unique_name(&mut self) { 521 | // Generate an unique name for the file based on the options, source, and the current version of manganis 522 | let uuid = self.hash(); 523 | let extension = self.options.extension(); 524 | let file_name = normalized_file_name(&self.location.source, extension); 525 | let extension = extension.map(|e| format!(".{e}")).unwrap_or_default(); 526 | self.location.unique_name = format!("{file_name}{uuid:x}{extension}"); 527 | assert!(self.location.unique_name.len() <= MAX_PATH_LENGTH); 528 | } 529 | } 530 | 531 | /// Create a normalized file name from the source 532 | fn normalized_file_name(location: &AssetSource, extension: Option<&str>) -> String { 533 | let last_segment = location.last_segment(); 534 | let mut file_name = to_alphanumeric_string_lossy(last_segment); 535 | 536 | let extension_len = extension.map(|e| e.len() + 1).unwrap_or_default(); 537 | let extension_and_hash_size = extension_len + HASH_SIZE; 538 | // If the file name is too long, we need to truncate it 539 | if file_name.len() + extension_and_hash_size > MAX_PATH_LENGTH { 540 | file_name = file_name[..MAX_PATH_LENGTH - extension_and_hash_size].to_string(); 541 | } 542 | file_name 543 | } 544 | 545 | /// Normalize a string to only contain alphanumeric characters 546 | fn to_alphanumeric_string_lossy(name: &str) -> String { 547 | name.chars() 548 | .filter(|c| c.is_alphanumeric()) 549 | .collect::() 550 | } 551 | 552 | fn hash_file(location: &AssetSource, hash: &mut DefaultHasher) { 553 | // Hash the last time the file was updated and the file source. If either of these change, we need to regenerate the unique name 554 | let updated = location.last_updated(); 555 | updated.hash(hash); 556 | location.hash(hash); 557 | } 558 | 559 | fn hash_version(hash: &mut DefaultHasher) { 560 | // Hash the current version of manganis. If this changes, we need to regenerate the unique name 561 | crate::built::PKG_VERSION.hash(hash); 562 | crate::built::GIT_COMMIT_HASH.hash(hash); 563 | } 564 | 565 | fn resolve_asset_location(location: &AssetLocation) -> Result { 566 | // If manganis is being used without CLI support, we will fallback to providing a local path. 567 | let manganis_support = std::env::var("MANGANIS_SUPPORT"); 568 | if manganis_support.is_err() { 569 | match location.source() { 570 | AssetSource::Remote(url) => Ok(url.as_str().to_string()), 571 | AssetSource::Local(path) => { 572 | // If this is not the main package, we can't include assets from it without CLI support 573 | let primary_package = std::env::var("CARGO_PRIMARY_PACKAGE").is_ok(); 574 | if !primary_package { 575 | return Err(ManganisSupportError::ExternalPackageCollection); 576 | } 577 | 578 | // Tauri doesn't allow absolute paths(??) so we convert to relative. 579 | let Ok(cwd) = std::env::var("CARGO_MANIFEST_DIR") else { 580 | return Err(ManganisSupportError::FailedToFindCargoManifest); 581 | }; 582 | 583 | // Windows adds `\\?\` to longer path names. We'll try to remove it. 584 | #[cfg(windows)] 585 | let path = { 586 | let path_as_string = path.display().to_string(); 587 | let path_as_string = path_as_string 588 | .strip_prefix("\\\\?\\") 589 | .unwrap_or(&path_as_string); 590 | PathBuf::from(path_as_string) 591 | }; 592 | 593 | let rel_path = path 594 | .strip_prefix(cwd) 595 | .map_err(|_| ManganisSupportError::FailedToFindCargoManifest)?; 596 | let path = PathBuf::from(".").join(rel_path); 597 | Ok(path.display().to_string()) 598 | } 599 | } 600 | } else { 601 | let config = Config::current(); 602 | let root = config.assets_serve_location(); 603 | let unique_name = location.unique_name(); 604 | Ok(format!("{root}{unique_name}")) 605 | } 606 | } 607 | 608 | /// An error that can occur while collecting assets without CLI support 609 | #[derive(Debug)] 610 | pub enum ManganisSupportError { 611 | /// An error that can occur while collecting assets from other packages without CLI support 612 | ExternalPackageCollection, 613 | /// Manganis failed to find the current package's manifest 614 | FailedToFindCargoManifest, 615 | } 616 | 617 | impl Display for ManganisSupportError { 618 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 619 | match self { 620 | Self::ExternalPackageCollection => write!(f, "Attempted to collect assets from other packages without a CLI that supports Manganis. Please recompile with a CLI that supports Manganis like the `dioxus-cli`."), 621 | Self::FailedToFindCargoManifest => write!(f, "Manganis failed to find the current package's manifest. Please recompile with a CLI that supports Manganis like the `dioxus-cli`."), 622 | } 623 | } 624 | } 625 | 626 | impl std::error::Error for ManganisSupportError {} 627 | 628 | /// A metadata asset 629 | #[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Clone)] 630 | pub struct MetadataAsset { 631 | key: String, 632 | value: String, 633 | } 634 | 635 | impl MetadataAsset { 636 | /// Creates a new metadata asset 637 | pub fn new(key: &str, value: &str) -> Self { 638 | Self { 639 | key: key.to_string(), 640 | value: value.to_string(), 641 | } 642 | } 643 | 644 | /// Returns the key of the metadata asset 645 | pub fn key(&self) -> &str { 646 | &self.key 647 | } 648 | 649 | /// Returns the value of the metadata asset 650 | pub fn value(&self) -> &str { 651 | &self.value 652 | } 653 | } 654 | 655 | /// A tailwind class asset 656 | #[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Clone)] 657 | pub struct TailwindAsset { 658 | classes: String, 659 | } 660 | 661 | impl TailwindAsset { 662 | /// Creates a new tailwind class asset 663 | pub fn new(classes: &str) -> Self { 664 | Self { 665 | classes: classes.to_string(), 666 | } 667 | } 668 | 669 | /// Returns the classes of the tailwind class asset 670 | pub fn classes(&self) -> &str { 671 | &self.classes 672 | } 673 | } 674 | --------------------------------------------------------------------------------