Cross platform asset resolver (#4521)
* add initial web resolver * re-export resolver and add playwright test * add documentation * fix clippy * fix docs + clippy
This commit is contained in:
1 parent
b72b142b53
commit
68645e7f0c
16 files changed
+607
-203
No files matched your search
Generated
+10
@@ -4966,6 +4966,7 @@ version = "0.7.0-alpha.3"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"criterion",
|
"criterion",
|
||||||
"dioxus",
|
"dioxus",
|
||||||
|
"dioxus-asset-resolver",
|
||||||
"dioxus-cli-config",
|
"dioxus-cli-config",
|
||||||
"dioxus-config-macro",
|
"dioxus-config-macro",
|
||||||
"dioxus-config-macros",
|
"dioxus-config-macros",
|
||||||
@@ -5005,15 +5006,22 @@ dependencies = [
|
|||||||
name = "dioxus-asset-resolver"
|
name = "dioxus-asset-resolver"
|
||||||
version = "0.7.0-alpha.3"
|
version = "0.7.0-alpha.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"dioxus",
|
||||||
"dioxus-cli-config",
|
"dioxus-cli-config",
|
||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
"infer",
|
"infer",
|
||||||
"jni 0.21.1",
|
"jni 0.21.1",
|
||||||
|
"js-sys",
|
||||||
|
"manganis-core",
|
||||||
"ndk 0.9.0",
|
"ndk 0.9.0",
|
||||||
"ndk-context",
|
"ndk-context",
|
||||||
"ndk-sys 0.6.0+11769913",
|
"ndk-sys 0.6.0+11769913",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
|
"serde_json",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
|
"tokio",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5223,6 +5231,8 @@ dependencies = [
|
|||||||
"dioxus",
|
"dioxus",
|
||||||
"flate2",
|
"flate2",
|
||||||
"reqwest 0.12.22",
|
"reqwest 0.12.22",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"tar",
|
"tar",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -11,14 +11,32 @@ keywords = ["web", "desktop", "mobile", "gui", "wasm"]
|
|||||||
rust-version = "1.79.0"
|
rust-version = "1.79.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
http = { workspace = true }
|
|
||||||
percent-encoding = { workspace = true }
|
|
||||||
infer = { workspace = true }
|
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
dioxus-cli-config = { workspace = true }
|
manganis-core = { workspace = true }
|
||||||
|
|
||||||
|
# native/fs resolver dependencies
|
||||||
|
http = { workspace = true, optional = true }
|
||||||
|
percent-encoding = { workspace = true, optional = true }
|
||||||
|
infer = { workspace = true, optional = true }
|
||||||
|
dioxus-cli-config = { workspace = true, optional = true }
|
||||||
|
tokio = { workspace = true, features = ["rt"], optional = true }
|
||||||
|
|
||||||
|
# browser resolver dependencies
|
||||||
|
web-sys = { workspace = true, features = ['Headers', 'Request', 'RequestInit', 'Response', 'Window'], optional = true }
|
||||||
|
js-sys = { workspace = true, optional = true }
|
||||||
|
wasm-bindgen-futures = { workspace = true, optional = true }
|
||||||
|
|
||||||
[target.'cfg(target_os = "android")'.dependencies]
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
jni = "0.21.1"
|
jni = { version = "0.21.1" }
|
||||||
ndk = { version = "0.9.0" }
|
ndk = { version = "0.9.0" }
|
||||||
ndk-sys = { version = "0.6.0" }
|
ndk-sys = { version = "0.6.0" }
|
||||||
ndk-context = { version = "0.1.1" }
|
ndk-context = { version = "0.1.1" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
dioxus = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
web = ["dep:web-sys", "dep:js-sys", "dep:wasm-bindgen-futures"]
|
||||||
|
native = ["dep:http", "dep:percent-encoding", "dep:infer", "dep:dioxus-cli-config", "dep:tokio"]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"key": "value"
|
||||||
|
}
|
||||||
+180
-193
@@ -1,209 +1,196 @@
|
|||||||
use http::{status::StatusCode, Response};
|
#![warn(missing_docs)]
|
||||||
use std::path::{Path, PathBuf};
|
//! The asset resolver for the Dioxus bundle format. Each platform has its own way of resolving assets. This crate handles
|
||||||
|
//! resolving assets in a cross-platform way.
|
||||||
|
//!
|
||||||
|
//! There are two broad locations for assets depending on the platform:
|
||||||
|
//! - **Web**: Assets are stored on a remote server and fetched via HTTP requests.
|
||||||
|
//! - **Native**: Assets are read from the local bundle. Each platform has its own bundle structure which may store assets
|
||||||
|
//! as a file at a specific path or in an opaque format like Android's AssetManager.
|
||||||
|
//!
|
||||||
|
//! [`read_asset_bytes`]( abstracts over both of these methods, allowing you to read the bytes of an asset
|
||||||
|
//! regardless of the platform.
|
||||||
|
//!
|
||||||
|
//! If you know you are on a desktop platform, you can use [`asset_path`] to resolve the path of an asset and read
|
||||||
|
//! the contents with [`std::fs`].
|
||||||
|
//!
|
||||||
|
//! ## Example
|
||||||
|
//! ```rust
|
||||||
|
//! # async fn asset_example() {
|
||||||
|
//! use dioxus::prelude::*;
|
||||||
|
//!
|
||||||
|
//! // Bundle the static JSON asset into the application
|
||||||
|
//! static JSON_ASSET: Asset = asset!("assets/data.json");
|
||||||
|
//!
|
||||||
|
//! // Read the bytes of the JSON asset
|
||||||
|
//! let bytes = dioxus::asset_resolver::read_asset_bytes(&JSON_ASSET).await.unwrap();
|
||||||
|
//!
|
||||||
|
//! // Deserialize the JSON data
|
||||||
|
//! let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
||||||
|
//! assert_eq!(json["key"].as_str(), Some("value"));
|
||||||
|
//! # }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use manganis_core::Asset;
|
||||||
|
use std::{fmt::Debug, path::PathBuf};
|
||||||
|
|
||||||
|
#[cfg(feature = "native")]
|
||||||
|
pub mod native;
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
mod web;
|
||||||
|
|
||||||
|
/// An error that can occur when resolving an asset to a path. Not all platforms can represent assets as paths,
|
||||||
|
/// an error may mean that the asset doesn't exist or it cannot be represented as a path.
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum AssetServeError {
|
pub enum AssetPathError {
|
||||||
#[error("Failed to infer mime type for asset: {0}")]
|
/// The asset was not found by the resolver.
|
||||||
InferringMimeType(std::io::Error),
|
#[error("Failed to find the path in the asset directory")]
|
||||||
|
NotFound,
|
||||||
|
|
||||||
#[error("Failed to serve asset: {0}")]
|
/// The asset may exist, but it cannot be represented as a path.
|
||||||
|
#[error("Asset cannot be represented as a path")]
|
||||||
|
CannotRepresentAsPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to resolve the path of an asset from a given URI path. Depending on the platform, this may
|
||||||
|
/// return an error even if the asset exists because some platforms cannot represent assets as paths.
|
||||||
|
/// You should prefer [`read_asset_bytes`] to read the asset bytes directly
|
||||||
|
/// for cross-platform compatibility.
|
||||||
|
///
|
||||||
|
/// ## Platform specific behavior
|
||||||
|
///
|
||||||
|
/// This function will only work on desktop platforms. It will always return an error in web and Android
|
||||||
|
/// bundles. On Android assets are bundled in the APK, and cannot be represented as paths. In web bundles,
|
||||||
|
/// Assets are fetched via HTTP requests and don't have a filesystem path.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// ```rust
|
||||||
|
/// use dioxus::prelude::*;
|
||||||
|
///
|
||||||
|
/// // Bundle the static JSON asset into the application
|
||||||
|
/// static JSON_ASSET: Asset = asset!("assets/data.json");
|
||||||
|
///
|
||||||
|
/// // Resolve the path of the asset. This will not work in web or Android bundles
|
||||||
|
/// let path = dioxus::asset_resolver::asset_path(&JSON_ASSET).unwrap();
|
||||||
|
///
|
||||||
|
/// println!("Asset path: {:?}", path);
|
||||||
|
///
|
||||||
|
/// // Read the bytes of the JSON asset
|
||||||
|
/// let bytes = std::fs::read(path).unwrap();
|
||||||
|
///
|
||||||
|
/// // Deserialize the JSON data
|
||||||
|
/// let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
||||||
|
/// assert_eq!(json["key"].as_str(), Some("value"));
|
||||||
|
/// ```
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn asset_path(asset: &Asset) -> Result<PathBuf, AssetPathError> {
|
||||||
|
#[cfg(all(feature = "web", target_arch = "wasm32"))]
|
||||||
|
return Err(AssetPathError::CannotRepresentAsPath);
|
||||||
|
|
||||||
|
#[cfg(feature = "native")]
|
||||||
|
return native::resolve_native_asset_path(asset.to_string().as_str());
|
||||||
|
|
||||||
|
Err(AssetPathError::NotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error that can occur when resolving an asset.
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum AssetResolveError {
|
||||||
|
/// An error occurred while resolving a native asset.
|
||||||
|
#[error("Failed to resolve native asset: {0}")]
|
||||||
|
Native(#[from] NativeAssetResolveError),
|
||||||
|
|
||||||
|
/// An error occurred while resolving a web asset.
|
||||||
|
#[error("Failed to resolve web asset: {0}")]
|
||||||
|
Web(#[from] WebAssetResolveError),
|
||||||
|
|
||||||
|
/// An error that occurs when no asset resolver is available for the current platform.
|
||||||
|
#[error("Asset resolution is not supported on this platform")]
|
||||||
|
UnsupportedPlatform,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the bytes of an asset. This will work on both web and native platforms. On the web,
|
||||||
|
/// it will fetch the asset via HTTP, and on native platforms, it will read the asset from the filesystem or bundle.
|
||||||
|
///
|
||||||
|
/// ## Errors
|
||||||
|
/// This function will return an error if the asset cannot be found or if it fails to read which may be due to I/O errors or
|
||||||
|
/// network issues.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # async fn asset_example() {
|
||||||
|
/// use dioxus::prelude::*;
|
||||||
|
///
|
||||||
|
/// // Bundle the static JSON asset into the application
|
||||||
|
/// static JSON_ASSET: Asset = asset!("assets/data.json");
|
||||||
|
///
|
||||||
|
/// // Read the bytes of the JSON asset
|
||||||
|
/// let bytes = dioxus::asset_resolver::read_asset_bytes(&JSON_ASSET).await.unwrap();
|
||||||
|
///
|
||||||
|
/// // Deserialize the JSON data
|
||||||
|
/// let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
||||||
|
/// assert_eq!(json["key"].as_str(), Some("value"));
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
#[allow(unused)]
|
||||||
|
pub async fn read_asset_bytes(asset: &Asset) -> Result<Vec<u8>, AssetResolveError> {
|
||||||
|
let path = asset.to_string();
|
||||||
|
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
return web::resolve_web_asset(&path)
|
||||||
|
.await
|
||||||
|
.map_err(AssetResolveError::Web);
|
||||||
|
|
||||||
|
#[cfg(feature = "native")]
|
||||||
|
return tokio::task::spawn_blocking(move || native::resolve_native_asset(&path))
|
||||||
|
.await
|
||||||
|
.map_err(|err| AssetResolveError::Native(NativeAssetResolveError::JoinError(err)))
|
||||||
|
.and_then(|result| result.map_err(AssetResolveError::Native));
|
||||||
|
|
||||||
|
Err(AssetResolveError::UnsupportedPlatform)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error that occurs when resolving a native asset.
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum NativeAssetResolveError {
|
||||||
|
/// An I/O error occurred while reading the asset from the filesystem.
|
||||||
|
#[error("Failed to read asset: {0}")]
|
||||||
IoError(#[from] std::io::Error),
|
IoError(#[from] std::io::Error),
|
||||||
|
|
||||||
#[error("Failed to construct response: {0}")]
|
/// The asset resolver failed to complete and could not be joined.
|
||||||
ResponseError(#[from] http::Error),
|
#[cfg(feature = "native")]
|
||||||
|
#[error("Asset resolver join failed: {0}")]
|
||||||
|
JoinError(tokio::task::JoinError),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serve an asset from the filesystem or a custom asset handler.
|
/// An error that occurs when resolving an asset on the web.
|
||||||
///
|
pub struct WebAssetResolveError {
|
||||||
/// This method properly accesses the asset directory based on the platform and serves the asset
|
#[cfg(feature = "web")]
|
||||||
/// wrapped in an HTTP response.
|
error: js_sys::Error,
|
||||||
///
|
|
||||||
/// Platform specifics:
|
|
||||||
/// - On the web, this returns AssetServerError since there's no filesystem access. Use `fetch` instead.
|
|
||||||
/// - On Android, it attempts to load assets using the Android AssetManager.
|
|
||||||
/// - On other platforms, it serves assets from the filesystem.
|
|
||||||
pub fn serve_asset(path: &str) -> Result<Response<Vec<u8>>, AssetServeError> {
|
|
||||||
// If the user provided a custom asset handler, then call it and return the response if the request was handled.
|
|
||||||
// The path is the first part of the URI, so we need to trim the leading slash.
|
|
||||||
let mut uri_path = PathBuf::from(
|
|
||||||
percent_encoding::percent_decode_str(path)
|
|
||||||
.decode_utf8()
|
|
||||||
.expect("expected URL to be UTF-8 encoded")
|
|
||||||
.as_ref(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Attempt to serve from the asset dir on android using its loader
|
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
{
|
|
||||||
if let Some(asset) = to_java_load_asset(path) {
|
|
||||||
return Ok(Response::builder()
|
|
||||||
.header("Content-Type", get_mime_by_ext(&uri_path))
|
|
||||||
.header("Access-Control-Allow-Origin", "*")
|
|
||||||
.body(asset)?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the asset doesn't exist, or starts with `/assets/`, then we'll try to serve out of the bundle
|
|
||||||
// This lets us handle both absolute and relative paths without being too "special"
|
|
||||||
// It just means that our macos bundle is a little "special" because we need to place an `assets`
|
|
||||||
// dir in the `Resources` dir.
|
|
||||||
//
|
|
||||||
// If there's no asset root, we use the cargo manifest dir as the root, or the current dir
|
|
||||||
if !uri_path.exists() || uri_path.starts_with("/assets/") {
|
|
||||||
let bundle_root = get_asset_root();
|
|
||||||
let relative_path = uri_path.strip_prefix("/").unwrap();
|
|
||||||
uri_path = bundle_root.join(relative_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the asset exists, then we can serve it!
|
|
||||||
if uri_path.exists() {
|
|
||||||
let mime_type =
|
|
||||||
get_mime_from_path(&uri_path).map_err(AssetServeError::InferringMimeType)?;
|
|
||||||
let body = std::fs::read(uri_path)?;
|
|
||||||
return Ok(Response::builder()
|
|
||||||
.header("Content-Type", mime_type)
|
|
||||||
.header("Access-Control-Allow-Origin", "*")
|
|
||||||
.body(body)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Response::builder()
|
|
||||||
.status(StatusCode::NOT_FOUND)
|
|
||||||
.body(String::from("Not Found").into_bytes())?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the asset directory, following tauri/cargo-bundles directory discovery approach
|
impl Debug for WebAssetResolveError {
|
||||||
///
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
/// Currently supports:
|
let mut debug = f.debug_struct("WebAssetResolveError");
|
||||||
/// - [x] macOS
|
#[cfg(feature = "web")]
|
||||||
/// - [x] iOS
|
debug.field("name", &self.error.name());
|
||||||
/// - [x] Windows
|
#[cfg(feature = "web")]
|
||||||
/// - [x] Linux (appimage)
|
debug.field("message", &self.error.message());
|
||||||
/// - [ ] Linux (rpm)
|
debug.finish()
|
||||||
/// - [x] Linux (deb)
|
|
||||||
/// - [ ] Android
|
|
||||||
#[allow(unreachable_code)]
|
|
||||||
fn get_asset_root() -> PathBuf {
|
|
||||||
let cur_exe = std::env::current_exe().unwrap();
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
return cur_exe
|
|
||||||
.parent()
|
|
||||||
.unwrap()
|
|
||||||
.parent()
|
|
||||||
.unwrap()
|
|
||||||
.join("Resources");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
// In linux bundles, the assets are placed in the lib/$product_name directory
|
|
||||||
// bin/
|
|
||||||
// main
|
|
||||||
// lib/
|
|
||||||
// $product_name/
|
|
||||||
// assets/
|
|
||||||
if let Some(product_name) = dioxus_cli_config::product_name() {
|
|
||||||
let lib_asset_path = || {
|
|
||||||
let path = cur_exe.parent()?.parent()?.join("lib").join(product_name);
|
|
||||||
path.exists().then_some(path)
|
|
||||||
};
|
|
||||||
if let Some(asset_dir) = lib_asset_path() {
|
|
||||||
return asset_dir;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For all others, the structure looks like this:
|
|
||||||
// app.(exe/appimage)
|
|
||||||
// main.exe
|
|
||||||
// assets/
|
|
||||||
cur_exe.parent().unwrap().to_path_buf()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the mime type from a path-like string
|
|
||||||
fn get_mime_from_path(asset: &Path) -> std::io::Result<&'static str> {
|
|
||||||
if asset.extension().is_some_and(|ext| ext == "svg") {
|
|
||||||
return Ok("image/svg+xml");
|
|
||||||
}
|
|
||||||
|
|
||||||
match infer::get_from_path(asset)?.map(|f| f.mime_type()) {
|
|
||||||
Some(f) if f != "text/plain" => Ok(f),
|
|
||||||
_other => Ok(get_mime_by_ext(asset)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the mime type from a URI using its extension
|
impl std::fmt::Display for WebAssetResolveError {
|
||||||
fn get_mime_by_ext(trimmed: &Path) -> &'static str {
|
#[allow(unreachable_code)]
|
||||||
match trimmed.extension().and_then(|e| e.to_str()) {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
// The common assets are all utf-8 encoded
|
#[cfg(feature = "web")]
|
||||||
Some("js") => "text/javascript; charset=utf-8",
|
return write!(f, "{}", self.error.message());
|
||||||
Some("css") => "text/css; charset=utf-8",
|
write!(f, "WebAssetResolveError")
|
||||||
Some("json") => "application/json; charset=utf-8",
|
|
||||||
Some("svg") => "image/svg+xml; charset=utf-8",
|
|
||||||
Some("html") => "text/html; charset=utf-8",
|
|
||||||
|
|
||||||
// the rest... idk? probably not
|
|
||||||
Some("mjs") => "text/javascript; charset=utf-8",
|
|
||||||
Some("bin") => "application/octet-stream",
|
|
||||||
Some("csv") => "text/csv",
|
|
||||||
Some("ico") => "image/vnd.microsoft.icon",
|
|
||||||
Some("jsonld") => "application/ld+json",
|
|
||||||
Some("rtf") => "application/rtf",
|
|
||||||
Some("mp4") => "video/mp4",
|
|
||||||
// Assume HTML when a TLD is found for eg. `dioxus:://dioxuslabs.app` | `dioxus://hello.com`
|
|
||||||
Some(_) => "text/html; charset=utf-8",
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
|
||||||
// using octet stream according to this:
|
|
||||||
None => "application/octet-stream",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
impl std::error::Error for WebAssetResolveError {}
|
||||||
pub(crate) fn to_java_load_asset(filepath: &str) -> Option<Vec<u8>> {
|
|
||||||
let normalized = filepath
|
|
||||||
.trim_start_matches("/assets/")
|
|
||||||
.trim_start_matches('/');
|
|
||||||
|
|
||||||
// in debug mode, the asset might be under `/data/local/tmp/dx/` - attempt to read it from there if it exists
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
{
|
|
||||||
let path = dioxus_cli_config::android_session_cache_dir().join(normalized);
|
|
||||||
if path.exists() {
|
|
||||||
return std::fs::read(path).ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
use std::ptr::NonNull;
|
|
||||||
|
|
||||||
let ctx = ndk_context::android_context();
|
|
||||||
let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }.unwrap();
|
|
||||||
let mut env = vm.attach_current_thread().unwrap();
|
|
||||||
|
|
||||||
// Query the Asset Manager
|
|
||||||
let asset_manager_ptr = env
|
|
||||||
.call_method(
|
|
||||||
unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) },
|
|
||||||
"getAssets",
|
|
||||||
"()Landroid/content/res/AssetManager;",
|
|
||||||
&[],
|
|
||||||
)
|
|
||||||
.expect("Failed to get asset manager")
|
|
||||||
.l()
|
|
||||||
.expect("Failed to get asset manager as object");
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
let asset_manager =
|
|
||||||
ndk_sys::AAssetManager_fromJava(env.get_native_interface(), *asset_manager_ptr);
|
|
||||||
|
|
||||||
let asset_manager = ndk::asset::AssetManager::from_ptr(
|
|
||||||
NonNull::new(asset_manager).expect("Invalid asset manager"),
|
|
||||||
);
|
|
||||||
|
|
||||||
let cstr = std::ffi::CString::new(normalized).unwrap();
|
|
||||||
|
|
||||||
let mut asset = asset_manager.open(&cstr)?;
|
|
||||||
Some(asset.buffer().unwrap().to_vec())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
//! Native specific utilities for resolving assets in a bundle. This module is intended for use in renderers that
|
||||||
|
//! need to resolve asset bundles for resources like images, and fonts.
|
||||||
|
|
||||||
|
use http::{status::StatusCode, Response};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::{AssetPathError, NativeAssetResolveError};
|
||||||
|
|
||||||
|
/// An error that can occur when serving an asset.
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum AssetServeError {
|
||||||
|
/// The asset path could not be resolved.
|
||||||
|
#[error("Failed to resolve asset: {0}")]
|
||||||
|
ResolveError(#[from] NativeAssetResolveError),
|
||||||
|
|
||||||
|
/// An error occurred while constructing the HTTP response.
|
||||||
|
#[error("Failed to construct response: {0}")]
|
||||||
|
ResponseError(#[from] http::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to resolve the path of an asset from a given URI path.
|
||||||
|
pub(crate) fn resolve_native_asset_path(path: &str) -> Result<PathBuf, AssetPathError> {
|
||||||
|
#[allow(clippy::unnecessary_lazy_evaluations)]
|
||||||
|
resolve_asset_path_from_filesystem(path).ok_or_else(|| {
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
// If the asset exists in the Android asset manager, return the can't be represented as a path
|
||||||
|
// error instead
|
||||||
|
if to_java_load_asset(path).is_some() {
|
||||||
|
return AssetPathError::CannotRepresentAsPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetPathError::NotFound
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to resolve the path of an asset from a given URI path.
|
||||||
|
fn resolve_asset_path_from_filesystem(path: &str) -> Option<PathBuf> {
|
||||||
|
// If the user provided a custom asset handler, then call it and return the response if the request was handled.
|
||||||
|
// The path is the first part of the URI, so we need to trim the leading slash.
|
||||||
|
let mut uri_path = PathBuf::from(
|
||||||
|
percent_encoding::percent_decode_str(path)
|
||||||
|
.decode_utf8()
|
||||||
|
.expect("expected URL to be UTF-8 encoded")
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// If the asset doesn't exist, or starts with `/assets/`, then we'll try to serve out of the bundle
|
||||||
|
// This lets us handle both absolute and relative paths without being too "special"
|
||||||
|
// It just means that our macos bundle is a little "special" because we need to place an `assets`
|
||||||
|
// dir in the `Resources` dir.
|
||||||
|
//
|
||||||
|
// If there's no asset root, we use the cargo manifest dir as the root, or the current dir
|
||||||
|
if !uri_path.exists() || uri_path.starts_with("/assets/") {
|
||||||
|
let bundle_root = get_asset_root();
|
||||||
|
let relative_path = uri_path.strip_prefix("/").unwrap();
|
||||||
|
uri_path = bundle_root.join(relative_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the asset exists, return it
|
||||||
|
uri_path.exists().then_some(uri_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ResolvedAsset {
|
||||||
|
mime_type: &'static str,
|
||||||
|
body: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResolvedAsset {
|
||||||
|
fn new(mime_type: &'static str, body: Vec<u8>) -> Self {
|
||||||
|
Self { mime_type, body }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_response(self) -> Result<Response<Vec<u8>>, AssetServeError> {
|
||||||
|
Ok(Response::builder()
|
||||||
|
.header("Content-Type", self.mime_type)
|
||||||
|
.header("Access-Control-Allow-Origin", "*")
|
||||||
|
.body(self.body)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the bytes for an asset
|
||||||
|
pub(crate) fn resolve_native_asset(path: &str) -> Result<Vec<u8>, NativeAssetResolveError> {
|
||||||
|
// Attempt to serve from the asset dir on android using its loader
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
if let Some(asset) = to_java_load_asset(path) {
|
||||||
|
return Ok(asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(uri_path) = resolve_asset_path_from_filesystem(path) else {
|
||||||
|
return Err(NativeAssetResolveError::IoError(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::NotFound,
|
||||||
|
"Asset not found",
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
Ok(std::fs::read(uri_path)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve the asset and its mime type
|
||||||
|
fn resolve_asset(path: &str) -> Result<Option<ResolvedAsset>, NativeAssetResolveError> {
|
||||||
|
// Attempt to serve from the asset dir on android using its loader
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
{
|
||||||
|
if let Some(asset) = to_java_load_asset(path) {
|
||||||
|
let extension = path.rsplit_once('.').and_then(|(_, ext)| Some(ext));
|
||||||
|
let mime_type = get_mime_from_ext(extension);
|
||||||
|
return Ok(Some(ResolvedAsset::new(mime_type, asset)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(uri_path) = resolve_asset_path_from_filesystem(path) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let mime_type = get_mime_from_path(&uri_path)?;
|
||||||
|
let body = std::fs::read(uri_path)?;
|
||||||
|
Ok(Some(ResolvedAsset::new(mime_type, body)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serve an asset from the filesystem or a custom asset handler.
|
||||||
|
///
|
||||||
|
/// This method properly accesses the asset directory based on the platform and serves the asset
|
||||||
|
/// wrapped in an HTTP response.
|
||||||
|
///
|
||||||
|
/// Platform specifics:
|
||||||
|
/// - On the web, this returns AssetServerError since there's no filesystem access. Use `fetch` instead.
|
||||||
|
/// - On Android, it attempts to load assets using the Android AssetManager.
|
||||||
|
/// - On other platforms, it serves assets from the filesystem.
|
||||||
|
pub fn serve_asset(path: &str) -> Result<Response<Vec<u8>>, AssetServeError> {
|
||||||
|
match resolve_asset(path)? {
|
||||||
|
Some(asset) => asset.into_response(),
|
||||||
|
None => Ok(Response::builder()
|
||||||
|
.status(StatusCode::NOT_FOUND)
|
||||||
|
.body(String::from("Not Found").into_bytes())?),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the asset directory, following tauri/cargo-bundles directory discovery approach
|
||||||
|
///
|
||||||
|
/// Currently supports:
|
||||||
|
/// - [x] macOS
|
||||||
|
/// - [x] iOS
|
||||||
|
/// - [x] Windows
|
||||||
|
/// - [x] Linux (appimage)
|
||||||
|
/// - [ ] Linux (rpm)
|
||||||
|
/// - [x] Linux (deb)
|
||||||
|
/// - [ ] Android
|
||||||
|
#[allow(unreachable_code)]
|
||||||
|
fn get_asset_root() -> PathBuf {
|
||||||
|
let cur_exe = std::env::current_exe().unwrap();
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
return cur_exe
|
||||||
|
.parent()
|
||||||
|
.unwrap()
|
||||||
|
.parent()
|
||||||
|
.unwrap()
|
||||||
|
.join("Resources");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
// In linux bundles, the assets are placed in the lib/$product_name directory
|
||||||
|
// bin/
|
||||||
|
// main
|
||||||
|
// lib/
|
||||||
|
// $product_name/
|
||||||
|
// assets/
|
||||||
|
if let Some(product_name) = dioxus_cli_config::product_name() {
|
||||||
|
let lib_asset_path = || {
|
||||||
|
let path = cur_exe.parent()?.parent()?.join("lib").join(product_name);
|
||||||
|
path.exists().then_some(path)
|
||||||
|
};
|
||||||
|
if let Some(asset_dir) = lib_asset_path() {
|
||||||
|
return asset_dir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For all others, the structure looks like this:
|
||||||
|
// app.(exe/appimage)
|
||||||
|
// main.exe
|
||||||
|
// assets/
|
||||||
|
cur_exe.parent().unwrap().to_path_buf()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the mime type from a path-like string
|
||||||
|
fn get_mime_from_path(asset: &Path) -> std::io::Result<&'static str> {
|
||||||
|
if asset.extension().is_some_and(|ext| ext == "svg") {
|
||||||
|
return Ok("image/svg+xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
match infer::get_from_path(asset)?.map(|f| f.mime_type()) {
|
||||||
|
Some(f) if f != "text/plain" => Ok(f),
|
||||||
|
_other => Ok(get_mime_by_ext(asset)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the mime type from a URI using its extension
|
||||||
|
fn get_mime_by_ext(trimmed: &Path) -> &'static str {
|
||||||
|
let ext = trimmed.extension().as_ref().and_then(|ext| ext.to_str());
|
||||||
|
get_mime_from_ext(ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the mime type from a URI using its extension
|
||||||
|
fn get_mime_from_ext(ext: Option<&str>) -> &'static str {
|
||||||
|
match ext {
|
||||||
|
// The common assets are all utf-8 encoded
|
||||||
|
Some("js") => "text/javascript; charset=utf-8",
|
||||||
|
Some("css") => "text/css; charset=utf-8",
|
||||||
|
Some("json") => "application/json; charset=utf-8",
|
||||||
|
Some("svg") => "image/svg+xml; charset=utf-8",
|
||||||
|
Some("html") => "text/html; charset=utf-8",
|
||||||
|
|
||||||
|
// the rest... idk? probably not
|
||||||
|
Some("mjs") => "text/javascript; charset=utf-8",
|
||||||
|
Some("bin") => "application/octet-stream",
|
||||||
|
Some("csv") => "text/csv",
|
||||||
|
Some("ico") => "image/vnd.microsoft.icon",
|
||||||
|
Some("jsonld") => "application/ld+json",
|
||||||
|
Some("rtf") => "application/rtf",
|
||||||
|
Some("mp4") => "video/mp4",
|
||||||
|
// Assume HTML when a TLD is found for eg. `dioxus:://dioxuslabs.app` | `dioxus://hello.com`
|
||||||
|
Some(_) => "text/html; charset=utf-8",
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
||||||
|
// using octet stream according to this:
|
||||||
|
None => "application/octet-stream",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
pub(crate) fn to_java_load_asset(filepath: &str) -> Option<Vec<u8>> {
|
||||||
|
let normalized = filepath
|
||||||
|
.trim_start_matches("/assets/")
|
||||||
|
.trim_start_matches('/');
|
||||||
|
|
||||||
|
// in debug mode, the asset might be under `/data/local/tmp/dx/` - attempt to read it from there if it exists
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
let path = dioxus_cli_config::android_session_cache_dir().join(normalized);
|
||||||
|
if path.exists() {
|
||||||
|
return std::fs::read(path).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use std::ptr::NonNull;
|
||||||
|
|
||||||
|
let ctx = ndk_context::android_context();
|
||||||
|
let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }.unwrap();
|
||||||
|
let mut env = vm.attach_current_thread().unwrap();
|
||||||
|
|
||||||
|
// Query the Asset Manager
|
||||||
|
let asset_manager_ptr = env
|
||||||
|
.call_method(
|
||||||
|
unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) },
|
||||||
|
"getAssets",
|
||||||
|
"()Landroid/content/res/AssetManager;",
|
||||||
|
&[],
|
||||||
|
)
|
||||||
|
.expect("Failed to get asset manager")
|
||||||
|
.l()
|
||||||
|
.expect("Failed to get asset manager as object");
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let asset_manager =
|
||||||
|
ndk_sys::AAssetManager_fromJava(env.get_native_interface(), *asset_manager_ptr);
|
||||||
|
|
||||||
|
let asset_manager = ndk::asset::AssetManager::from_ptr(
|
||||||
|
NonNull::new(asset_manager).expect("Invalid asset manager"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let cstr = std::ffi::CString::new(normalized).unwrap();
|
||||||
|
|
||||||
|
let mut asset = asset_manager.open(&cstr)?;
|
||||||
|
Some(asset.buffer().unwrap().to_vec())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
use js_sys::{
|
||||||
|
wasm_bindgen::{JsCast, JsValue},
|
||||||
|
ArrayBuffer, Uint8Array,
|
||||||
|
};
|
||||||
|
use wasm_bindgen_futures::JsFuture;
|
||||||
|
use web_sys::{Request, Response};
|
||||||
|
|
||||||
|
use crate::WebAssetResolveError;
|
||||||
|
|
||||||
|
impl From<js_sys::Error> for WebAssetResolveError {
|
||||||
|
fn from(error: js_sys::Error) -> Self {
|
||||||
|
WebAssetResolveError { error }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebAssetResolveError {
|
||||||
|
fn from_js_value(value: JsValue) -> Self {
|
||||||
|
if let Some(error) = value.dyn_ref::<js_sys::Error>() {
|
||||||
|
WebAssetResolveError::from(error.clone())
|
||||||
|
} else {
|
||||||
|
unreachable!("Expected a js_sys::Error, got: {:?}", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn resolve_web_asset(path: &str) -> Result<Vec<u8>, WebAssetResolveError> {
|
||||||
|
let url = if path.starts_with("/") {
|
||||||
|
path.to_string()
|
||||||
|
} else {
|
||||||
|
format!("/{path}")
|
||||||
|
};
|
||||||
|
|
||||||
|
let request = Request::new_with_str(&url).map_err(WebAssetResolveError::from_js_value)?;
|
||||||
|
|
||||||
|
let window = web_sys::window().unwrap();
|
||||||
|
let response_promise = JsFuture::from(window.fetch_with_request(&request))
|
||||||
|
.await
|
||||||
|
.map_err(WebAssetResolveError::from_js_value)?;
|
||||||
|
let response = response_promise.unchecked_into::<Response>();
|
||||||
|
|
||||||
|
let array_buffer_promise = response
|
||||||
|
.array_buffer()
|
||||||
|
.map_err(WebAssetResolveError::from_js_value)?;
|
||||||
|
let array_buffer: ArrayBuffer = JsFuture::from(array_buffer_promise)
|
||||||
|
.await
|
||||||
|
.map_err(WebAssetResolveError::from_js_value)?
|
||||||
|
.unchecked_into();
|
||||||
|
let bytes = Uint8Array::new(&array_buffer);
|
||||||
|
Ok(bytes.to_vec())
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ dioxus-document = { workspace = true }
|
|||||||
dioxus-signals = { workspace = true, optional = true }
|
dioxus-signals = { workspace = true, optional = true }
|
||||||
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol", "serialize"] }
|
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol", "serialize"] }
|
||||||
dioxus-cli-config = { workspace = true }
|
dioxus-cli-config = { workspace = true }
|
||||||
dioxus-asset-resolver = { workspace = true }
|
dioxus-asset-resolver = { workspace = true, features = ["native"] }
|
||||||
generational-box = { workspace = true }
|
generational-box = { workspace = true }
|
||||||
dioxus-devtools = { workspace = true, optional = true }
|
dioxus-devtools = { workspace = true, optional = true }
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ pub(super) fn desktop_handler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match dioxus_asset_resolver::serve_asset(request.uri().path()) {
|
match dioxus_asset_resolver::native::serve_asset(request.uri().path()) {
|
||||||
Ok(res) => responder.respond(res),
|
Ok(res) => responder.respond(res),
|
||||||
Err(_e) => responder.respond(
|
Err(_e) => responder.respond(
|
||||||
Response::builder()
|
Response::builder()
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ dioxus-server = { workspace = true, optional = true }
|
|||||||
dioxus-ssr = { workspace = true, optional = true }
|
dioxus-ssr = { workspace = true, optional = true }
|
||||||
dioxus-native = { workspace = true, optional = true }
|
dioxus-native = { workspace = true, optional = true }
|
||||||
dioxus_server_macro = { workspace = true, optional = true }
|
dioxus_server_macro = { workspace = true, optional = true }
|
||||||
|
dioxus-asset-resolver = { workspace = true, optional = true }
|
||||||
manganis = { workspace = true, features = ["dioxus"], optional = true }
|
manganis = { workspace = true, features = ["dioxus"], optional = true }
|
||||||
dioxus-logger = { workspace = true, optional = true }
|
dioxus-logger = { workspace = true, optional = true }
|
||||||
warnings = { workspace = true, optional = true }
|
warnings = { workspace = true, optional = true }
|
||||||
@@ -69,7 +70,7 @@ hooks = ["dep:dioxus-hooks"]
|
|||||||
devtools = ["dep:dioxus-devtools", "dioxus-web?/devtools", "dioxus-fullstack?/devtools"]
|
devtools = ["dep:dioxus-devtools", "dioxus-web?/devtools", "dioxus-fullstack?/devtools"]
|
||||||
mounted = ["dioxus-web?/mounted"]
|
mounted = ["dioxus-web?/mounted"]
|
||||||
file_engine = ["dioxus-web?/file_engine"]
|
file_engine = ["dioxus-web?/file_engine"]
|
||||||
asset = ["dep:manganis"]
|
asset = ["dep:manganis", "dep:dioxus-asset-resolver"]
|
||||||
document = ["dioxus-web?/document", "dep:dioxus-document", "dep:dioxus-history"]
|
document = ["dioxus-web?/document", "dep:dioxus-document", "dep:dioxus-history"]
|
||||||
logger = ["dep:dioxus-logger"]
|
logger = ["dep:dioxus-logger"]
|
||||||
cli-config = ["dep:dioxus-cli-config"]
|
cli-config = ["dep:dioxus-cli-config"]
|
||||||
@@ -92,6 +93,7 @@ web = [
|
|||||||
"dioxus-config-macro/web",
|
"dioxus-config-macro/web",
|
||||||
"dep:dioxus-cli-config",
|
"dep:dioxus-cli-config",
|
||||||
"dioxus-cli-config?/web",
|
"dioxus-cli-config?/web",
|
||||||
|
"dioxus-asset-resolver?/web",
|
||||||
]
|
]
|
||||||
ssr = ["dep:dioxus-ssr", "dioxus-config-macro/ssr"]
|
ssr = ["dep:dioxus-ssr", "dioxus-config-macro/ssr"]
|
||||||
liveview = ["dep:dioxus-liveview", "dioxus-config-macro/liveview"]
|
liveview = ["dep:dioxus-liveview", "dioxus-config-macro/liveview"]
|
||||||
|
|||||||
@@ -129,6 +129,11 @@ pub use wasm_splitter as wasm_split;
|
|||||||
|
|
||||||
pub use subsecond;
|
pub use subsecond;
|
||||||
|
|
||||||
|
#[cfg(feature = "asset")]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(feature = "asset")))]
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use dioxus_asset_resolver as asset_resolver;
|
||||||
|
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
#[cfg(feature = "document")]
|
#[cfg(feature = "document")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "document")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "document")))]
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ blitz-shell = { workspace = true }
|
|||||||
dioxus-core = { workspace = true }
|
dioxus-core = { workspace = true }
|
||||||
dioxus-html = { workspace = true }
|
dioxus-html = { workspace = true }
|
||||||
dioxus-native-dom = { workspace = true }
|
dioxus-native-dom = { workspace = true }
|
||||||
dioxus-asset-resolver = { workspace = true }
|
dioxus-asset-resolver = { workspace = true, features = ["native"] }
|
||||||
dioxus-cli-config = { workspace = true, optional = true }
|
dioxus-cli-config = { workspace = true, optional = true }
|
||||||
dioxus-devtools = { workspace = true, optional = true }
|
dioxus-devtools = { workspace = true, optional = true }
|
||||||
dioxus-history = { workspace = true }
|
dioxus-history = { workspace = true }
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ impl NetProvider<Resource> for DioxusNativeNetProvider {
|
|||||||
handler: blitz_traits::net::BoxedHandler<Resource>,
|
handler: blitz_traits::net::BoxedHandler<Resource>,
|
||||||
) {
|
) {
|
||||||
if request.url.scheme() == "dioxus" {
|
if request.url.scheme() == "dioxus" {
|
||||||
match dioxus_asset_resolver::serve_asset(request.url.path()) {
|
match dioxus_asset_resolver::native::serve_asset(request.url.path()) {
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
tracing::trace!("fetching asset from file system success {request:#?}");
|
tracing::trace!("fetching asset from file system success {request:#?}");
|
||||||
handler.bytes(doc_id, res.into_body().into(), self.callback.clone())
|
handler.bytes(doc_id, res.into_body().into(), self.callback.clone())
|
||||||
|
|||||||
@@ -45,3 +45,15 @@ test("unused external assets are bundled", async ({ page }) => {
|
|||||||
// make sure the response is an image
|
// make sure the response is an image
|
||||||
expect(response.headers()["content-type"]).toBe("image/png");
|
expect(response.headers()["content-type"]).toBe("image/png");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("assets are resolved", async ({ page }) => {
|
||||||
|
await page.goto("http://localhost:8989");
|
||||||
|
|
||||||
|
// Expect the page to contain an element with the id "resolved-data"
|
||||||
|
const resolvedData = page.locator("#resolved-data");
|
||||||
|
await expect(resolvedData).toBeVisible();
|
||||||
|
// Expect the element to contain the text "List: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"
|
||||||
|
await expect(resolvedData).toContainText(
|
||||||
|
"List: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -8,6 +8,8 @@ publish = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus = { workspace = true, features = ["web"] }
|
dioxus = { workspace = true, features = ["web"] }
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
serde_json.workspace = true
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
reqwest = { workspace = true, features = ["blocking"] }
|
reqwest = { workspace = true, features = ["blocking"] }
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"list": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||||
|
}
|
||||||
@@ -56,5 +56,36 @@ fn App() -> Element {
|
|||||||
id: "some_image_without_hash",
|
id: "some_image_without_hash",
|
||||||
src: "{SOME_IMAGE_WITHOUT_HASH}"
|
src: "{SOME_IMAGE_WITHOUT_HASH}"
|
||||||
}
|
}
|
||||||
|
LoadsAsset {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const JSON: Asset = asset!("/assets/data.json");
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Deserialize)]
|
||||||
|
struct Data {
|
||||||
|
list: Vec<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn LoadsAsset() -> Element {
|
||||||
|
let data = use_resource(|| async {
|
||||||
|
let bytes = dioxus::asset_resolver::read_asset_bytes(&JSON)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
serde_json::from_slice::<Data>(&bytes).unwrap()
|
||||||
|
});
|
||||||
|
match data() {
|
||||||
|
Some(data) => rsx! {
|
||||||
|
div {
|
||||||
|
id: "resolved-data",
|
||||||
|
"List: {data.list:?}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => rsx! {
|
||||||
|
div {
|
||||||
|
"Loading..."
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user