From ba4389567df3b5150b471b60c623dad48014e16d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 13 Nov 2024 19:52:23 -0500 Subject: [PATCH] Deprecate relative asset paths, better warnings for asset!() (#3214) * Deprecate relative asset paths * fix paths, better warnings in asset parser --- .../ecommerce-site/src/components/loading.rs | 2 +- example-projects/ecommerce-site/src/main.rs | 2 +- example-projects/file-explorer/src/main.rs | 2 +- .../fullstack-hackernews/src/main.rs | 2 +- examples/calculator_mutable.rs | 2 +- examples/crm.rs | 2 +- examples/read_size.rs | 2 +- examples/resize.rs | 2 +- packages/document/src/elements/link.rs | 2 +- packages/document/src/elements/script.rs | 2 +- packages/manganis/manganis-core/src/asset.rs | 63 ++++++++++++++----- packages/manganis/manganis-macro/src/asset.rs | 38 +++++++---- packages/manganis/manganis/src/builder.rs | 10 +++ 13 files changed, 95 insertions(+), 36 deletions(-) diff --git a/example-projects/ecommerce-site/src/components/loading.rs b/example-projects/ecommerce-site/src/components/loading.rs index cd387ba61..2f4b1f2a3 100644 --- a/example-projects/ecommerce-site/src/components/loading.rs +++ b/example-projects/ecommerce-site/src/components/loading.rs @@ -5,7 +5,7 @@ pub(crate) fn ChildrenOrLoading(children: Element) -> Element { rsx! { document::Link { rel: "stylesheet", - href: asset!("./public/loading.css") + href: asset!("/public/loading.css") } SuspenseBoundary { fallback: |context: SuspenseContext| { diff --git a/example-projects/ecommerce-site/src/main.rs b/example-projects/ecommerce-site/src/main.rs index c6c021d3a..1ff12b67c 100644 --- a/example-projects/ecommerce-site/src/main.rs +++ b/example-projects/ecommerce-site/src/main.rs @@ -19,7 +19,7 @@ fn main() { rsx! { document::Link { rel: "stylesheet", - href: asset!("./public/tailwind.css") + href: asset!("/public/tailwind.css") } ChildrenOrLoading { diff --git a/example-projects/file-explorer/src/main.rs b/example-projects/file-explorer/src/main.rs index 73c35338b..8da1b5d00 100644 --- a/example-projects/file-explorer/src/main.rs +++ b/example-projects/file-explorer/src/main.rs @@ -23,7 +23,7 @@ fn app() -> Element { rsx! { document::Link { rel: "stylesheet", - href: asset!("./assets/fileexplorer.css") + href: asset!("/assets/fileexplorer.css") } div { document::Link { href: "https://fonts.googleapis.com/icon?family=Material+Icons", rel: "stylesheet" } diff --git a/example-projects/fullstack-hackernews/src/main.rs b/example-projects/fullstack-hackernews/src/main.rs index c36ad3e82..3bb1c0927 100644 --- a/example-projects/fullstack-hackernews/src/main.rs +++ b/example-projects/fullstack-hackernews/src/main.rs @@ -36,7 +36,7 @@ pub fn App() -> Element { #[component] fn Homepage(story: ReadOnlySignal) -> Element { rsx! { - document::Link { rel: "stylesheet", href: asset!("./assets/hackernews.css") } + document::Link { rel: "stylesheet", href: asset!("/assets/hackernews.css") } div { display: "flex", flex_direction: "row", width: "100%", div { width: "50%", diff --git a/examples/calculator_mutable.rs b/examples/calculator_mutable.rs index 35dfed3f5..825851d37 100644 --- a/examples/calculator_mutable.rs +++ b/examples/calculator_mutable.rs @@ -31,7 +31,7 @@ fn app() -> Element { rsx! { document::Link { rel: "stylesheet", - href: asset!("./examples/assets/calculator.css"), + href: asset!("/examples/assets/calculator.css"), } div { id: "wrapper", div { class: "app", diff --git a/examples/crm.rs b/examples/crm.rs index 294bb33d8..8469b2e16 100644 --- a/examples/crm.rs +++ b/examples/crm.rs @@ -32,7 +32,7 @@ fn main() { } document::Link { rel: "stylesheet", - href: asset!("./examples/assets/crm.css"), + href: asset!("/examples/assets/crm.css"), } h1 { "Dioxus CRM Example" } Router:: {} diff --git a/examples/read_size.rs b/examples/read_size.rs index f4a6fe510..4a3f0ad4b 100644 --- a/examples/read_size.rs +++ b/examples/read_size.rs @@ -30,7 +30,7 @@ fn app() -> Element { rsx! { document::Link { rel: "stylesheet", - href: asset!("./examples/assets/read_size.css"), + href: asset!("/examples/assets/read_size.css"), } div { width: "50%", diff --git a/examples/resize.rs b/examples/resize.rs index 895b6752d..97bdfb52a 100644 --- a/examples/resize.rs +++ b/examples/resize.rs @@ -17,7 +17,7 @@ fn app() -> Element { rsx!( document::Link { rel: "stylesheet", - href: asset!("./examples/assets/read_size.css"), + href: asset!("/examples/assets/read_size.css"), } div { width: "50%", diff --git a/packages/document/src/elements/link.rs b/packages/document/src/elements/link.rs index 27fd3d1eb..749d4cc69 100644 --- a/packages/document/src/elements/link.rs +++ b/packages/document/src/elements/link.rs @@ -86,7 +86,7 @@ impl LinkProps { /// // You can use the meta component to render a meta tag into the head of the page /// // This meta tag will redirect the user to the dioxuslabs homepage in 10 seconds /// document::Link { -/// href: asset!("./assets/style.css"), +/// href: asset!("/assets/style.css"), /// rel: "stylesheet", /// } /// } diff --git a/packages/document/src/elements/script.rs b/packages/document/src/elements/script.rs index 47f78fc6b..6bc22f722 100644 --- a/packages/document/src/elements/script.rs +++ b/packages/document/src/elements/script.rs @@ -74,7 +74,7 @@ impl ScriptProps { /// rsx! { /// // You can use the Script component to render a script tag into the head of the page /// document::Script { -/// src: asset!("./assets/script.js"), +/// src: asset!("/assets/script.js"), /// } /// } /// } diff --git a/packages/manganis/manganis-core/src/asset.rs b/packages/manganis/manganis-core/src/asset.rs index d0973620f..cf4e83c5f 100644 --- a/packages/manganis/manganis-core/src/asset.rs +++ b/packages/manganis/manganis-core/src/asset.rs @@ -24,11 +24,8 @@ pub struct ResourceAsset { pub bundled: String, } -#[derive(Debug)] -pub struct AssetError {} - impl ResourceAsset { - pub fn parse_any(raw: &str) -> Result { + pub fn parse_any(raw: &str) -> Result { // get the location where the asset is absolute, relative to // // IE @@ -43,13 +40,17 @@ impl ResourceAsset { let input = PathBuf::from(raw); // 2. absolute path to the asset - let absolute = manifest_dir - .join(raw.trim_start_matches('/')) - .canonicalize() - .unwrap(); + let absolute = manifest_dir.join(raw.trim_start_matches('/')); + let absolute = + absolute + .canonicalize() + .map_err(|err| AssetParseError::AssetDoesntExist { + err, + path: absolute, + })?; // 3. the bundled path is the unique name of the asset - let bundled = Self::make_unique_name(absolute.clone()); + let bundled = Self::make_unique_name(absolute.clone())?; Ok(Self { input, @@ -58,14 +59,16 @@ impl ResourceAsset { }) } - fn make_unique_name(file_path: PathBuf) -> String { + fn make_unique_name(file_path: PathBuf) -> Result { // Create a hasher let mut hash = std::collections::hash_map::DefaultHasher::new(); // Open the file to get its options - let file = std::fs::File::open(&file_path).unwrap(); - let metadata = file.metadata().unwrap(); - let modified = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH); + let file = std::fs::File::open(&file_path).map_err(AssetParseError::FailedToReadAsset)?; + let modified = file + .metadata() + .and_then(|metadata| metadata.modified()) + .unwrap_or(SystemTime::UNIX_EPOCH); // Hash a bunch of metadata // name, options, modified time, and maybe the version of our crate @@ -74,12 +77,42 @@ impl ResourceAsset { file_path.hash(&mut hash); let uuid = hash.finish(); - let file_name = file_path.file_stem().unwrap().to_string_lossy(); + let file_name = file_path + .file_stem() + .expect("file_path should have a file_stem") + .to_string_lossy(); + let extension = file_path .extension() .map(|f| f.to_string_lossy()) .unwrap_or_default(); - format!("{file_name}-{uuid:x}.{extension}") + Ok(format!("{file_name}-{uuid:x}.{extension}")) + } +} + +#[derive(Debug)] +pub enum AssetParseError { + ParseError(String), + AssetDoesntExist { + err: std::io::Error, + path: std::path::PathBuf, + }, + FailedToReadAsset(std::io::Error), + FailedToReadMetadata(std::io::Error), +} + +impl std::fmt::Display for AssetParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AssetParseError::ParseError(err) => write!(f, "Failed to parse asset: {}", err), + AssetParseError::AssetDoesntExist { err, path } => { + write!(f, "Asset at {} doesn't exist: {}", path.display(), err) + } + AssetParseError::FailedToReadAsset(err) => write!(f, "Failed to read asset: {}", err), + AssetParseError::FailedToReadMetadata(err) => { + write!(f, "Failed to read asset metadata: {}", err) + } + } } } diff --git a/packages/manganis/manganis-macro/src/asset.rs b/packages/manganis/manganis-macro/src/asset.rs index a77325b68..f952281bc 100644 --- a/packages/manganis/manganis-macro/src/asset.rs +++ b/packages/manganis/manganis-macro/src/asset.rs @@ -1,6 +1,6 @@ -use manganis_core::ResourceAsset; +use manganis_core::{AssetParseError, ResourceAsset}; use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens}; +use quote::{quote, ToTokens, TokenStreamExt}; use syn::{ parse::{Parse, ParseStream}, LitStr, @@ -8,7 +8,7 @@ use syn::{ pub struct AssetParser { /// The asset itself - asset: ResourceAsset, + asset: Result, /// The source of the trailing options options: TokenStream2, @@ -19,13 +19,13 @@ impl Parse for AssetParser { // // This gives you the Asset type - it's generic and basically unrefined // ``` - // asset!("myfile.png") + // asset!("/assets/myfile.png") // ``` // // To narrow the type, use a method call to get the refined type // ``` // asset!( - // "myfile.png", + // "/assets/myfile.png", // asset::image() // .format(ImageType::Jpg) // .size(512, 512) @@ -36,7 +36,7 @@ impl Parse for AssetParser { fn parse(input: ParseStream) -> syn::Result { // And then parse the options let src = input.parse::()?; - let asset = ResourceAsset::parse_any(&src.value()).unwrap(); + let asset = ResourceAsset::parse_any(&src.value()); let options = input.parse()?; Ok(Self { asset, options }) @@ -60,23 +60,39 @@ impl ToTokens for AssetParser { // a limitation from rust itself. We technically could support them but not without some hoops // to jump through fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let asset = match self.asset.as_ref() { + Ok(asset) => asset, + Err(err) => { + let err = err.to_string(); + tokens.append_all(quote! { compile_error!(#err) }); + return; + } + }; + // 1. the link section itself - let link_section = crate::generate_link_section(&self.asset); + let link_section = crate::generate_link_section(&asset); // 2. original - let input = self.asset.input.display().to_string(); + let input = asset.input.display().to_string(); // 3. resolved on the user's system - let local = self.asset.absolute.display().to_string(); + let local = asset.absolute.display().to_string(); // 4. bundled - let bundled = self.asset.bundled.to_string(); + let bundled = asset.bundled.to_string(); // 5. source tokens let option_source = &self.options; + // generate the asset::new method to deprecate the `./assets/blah.css` syntax + let method = if asset.input.is_relative() { + quote::quote! { new_relative } + } else { + quote::quote! { new } + }; + tokens.extend(quote! { - Asset::new( + Asset::#method( { #link_section manganis::Asset { diff --git a/packages/manganis/manganis/src/builder.rs b/packages/manganis/manganis/src/builder.rs index 1441fd734..75513ac24 100644 --- a/packages/manganis/manganis/src/builder.rs +++ b/packages/manganis/manganis/src/builder.rs @@ -22,6 +22,16 @@ impl Asset { self } + /// Create a new asset but with a relative path + /// + /// This method is deprecated and will be removed in a future release. + #[deprecated( + note = "Relative asset!() paths are not supported. Use a path like `/assets/myfile.png` instead of `./assets/myfile.png`" + )] + pub const fn new_relative(self) -> Self { + self + } + /// Get the path to the asset pub fn path(&self) -> PathBuf { PathBuf::from(self.input.to_string())