Browse Source

Deprecate relative asset paths, better warnings for asset!() (#3214)

* Deprecate relative asset paths

* fix paths, better warnings in asset parser
Jonathan Kelley 7 months ago
parent
commit
ba4389567d

+ 1 - 1
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| {

+ 1 - 1
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 {

+ 1 - 1
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" }

+ 1 - 1
example-projects/fullstack-hackernews/src/main.rs

@@ -36,7 +36,7 @@ pub fn App() -> Element {
 #[component]
 fn Homepage(story: ReadOnlySignal<PreviewState>) -> 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%",

+ 1 - 1
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",

+ 1 - 1
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::<Route> {}

+ 1 - 1
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%",

+ 1 - 1
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%",

+ 1 - 1
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",
 ///         }
 ///     }

+ 1 - 1
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"),
 ///         }
 ///     }
 /// }

+ 48 - 15
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<Self, AssetError> {
+    pub fn parse_any(raw: &str) -> Result<Self, AssetParseError> {
         // 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<String, AssetParseError> {
         // 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)
+            }
+        }
     }
 }

+ 27 - 11
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<ResourceAsset, AssetParseError>,
 
     /// 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<Self> {
         // And then parse the options
         let src = input.parse::<LitStr>()?;
-        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 {

+ 10 - 0
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())