Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions packages/asset-resolver/src/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ pub(crate) fn resolve_native_asset_path(path: &str) -> Result<PathBuf, AssetPath
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(
let uri_path = PathBuf::from(
percent_encoding::percent_decode_str(path)
.decode_utf8()
.expect("expected URL to be UTF-8 encoded")
Expand All @@ -55,8 +55,19 @@ fn resolve_asset_path_from_filesystem(path: &str) -> Option<PathBuf> {
// 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);
let relative_path = uri_path.strip_prefix("/").unwrap_or(&uri_path);

// First attempt: resolve directly relative to the bundle root (eg `.well-known/...`).
let direct_candidate = bundle_root.join(relative_path);
if direct_candidate.exists() {
return Some(direct_candidate);
}

// Fallback: look inside the conventional `assets/` directory.
let assets_candidate = bundle_root.join("assets").join(relative_path);
if assets_candidate.exists() {
return Some(assets_candidate);
}
}

// If the asset exists, return it
Expand Down
41 changes: 28 additions & 13 deletions packages/cli/src/build/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -645,8 +645,6 @@ impl AppBuilder {
let original = self.build.main_exe();
let new = self.build.patch_exe(res.time_start);
let triple = self.build.triple.clone();
let asset_dir = self.build.asset_dir();

// Hotpatch asset!() calls
for bundled in res.assets.unique_assets() {
let original_artifacts = self
Expand All @@ -663,7 +661,7 @@ impl AppBuilder {

let from = dunce::canonicalize(PathBuf::from(bundled.absolute_source_path()))?;

let to = asset_dir.join(bundled.bundled_path());
let to = self.build.asset_destination_path(bundled);

tracing::debug!("Copying asset from patch: {}", from.display());
if let Err(e) = dioxus_cli_opt::process_file_to(bundled.options(), &from, &to) {
Expand Down Expand Up @@ -756,11 +754,6 @@ impl AppBuilder {

// Use the build dir if there's no runtime asset dir as the override. For the case of ios apps,
// we won't actually be using the build dir.
let asset_dir = match self.runtime_asset_dir.as_ref() {
Some(dir) => dir.to_path_buf().join("assets/"),
None => self.build.asset_dir(),
};

// Canonicalize the path as Windows may use long-form paths "\\\\?\\C:\\".
let changed_file = dunce::canonicalize(changed_file)
.inspect_err(|e| tracing::debug!("Failed to canonicalize hotreloaded asset: {e}"))
Expand All @@ -770,9 +763,19 @@ impl AppBuilder {
let resources = artifacts.assets.get_assets_for_source(&changed_file)?;
let mut bundled_names = Vec::new();
for resource in resources {
let output_path = asset_dir.join(resource.bundled_path());
let output_path = if let Some(runtime_dir) = self.runtime_asset_dir.as_ref() {
let mut base = runtime_dir.to_path_buf().join("assets");
if let Some(mount) = self.build.normalized_mount_path(resource.options()) {
if !mount.as_os_str().is_empty() {
base = base.join(mount);
}
}
base.join(resource.bundled_path())
} else {
self.build.asset_destination_path(resource)
};

tracing::debug!("Hotreloading asset {changed_file:?} in target {asset_dir:?}");
tracing::debug!("Hotreloading asset {changed_file:?} into {output_path:?}");

// Remove the old asset if it exists
_ = std::fs::remove_file(&output_path);
Expand All @@ -782,15 +785,27 @@ impl AppBuilder {
// hotreloading, we need to use the old asset location it was originally written to.
let options = *resource.options();
let res = process_file_to(&options, &changed_file, &output_path);
let bundled_name = PathBuf::from(resource.bundled_path());
let bundled_name = self.build.asset_public_path(resource);
if let Err(e) = res {
tracing::debug!("Failed to hotreload asset {e}");
}

// If the emulator is android, we need to copy the asset to the device with `adb push asset /data/local/tmp/dx/assets/filename.ext`
if self.build.bundle == BundleFormat::Android {
_ = self
.copy_file_to_android_tmp(&changed_file, &bundled_name)
let mut android_relative = bundled_name.clone();
while android_relative.has_root() {
android_relative = android_relative
.strip_prefix(Path::new("/"))
.unwrap_or(android_relative.as_path())
.to_path_buf();
}

if let Ok(stripped) = android_relative.strip_prefix("assets") {
android_relative = stripped.to_path_buf();
}

let _ = self
.copy_file_to_android_tmp(&changed_file, &android_relative)
.await;
}
bundled_names.push(bundled_name);
Expand Down
95 changes: 87 additions & 8 deletions packages/cli/src/build/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,15 +333,15 @@ use dioxus_cli_config::{APP_TITLE_ENV, ASSET_ROOT_ENV};
use dioxus_cli_opt::{process_file_to, AssetManifest};
use itertools::Itertools;
use krates::{cm::TargetKind, NodeId};
use manganis::AssetOptions;
use manganis::{AssetOptions, BundledAsset};
use manganis_core::AssetVariant;
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use serde::{Deserialize, Serialize};
use std::{borrow::Cow, ffi::OsString};
use std::{
collections::{BTreeMap, HashSet},
io::Write,
path::{Path, PathBuf},
path::{Component, Path, PathBuf},
process::Stdio,
sync::{
atomic::{AtomicUsize, Ordering},
Expand Down Expand Up @@ -1471,7 +1471,7 @@ impl BuildRequest {
// Create a set of all the paths that new files will be bundled to
let mut keep_bundled_output_paths: HashSet<_> = assets
.unique_assets()
.map(|a| asset_dir.join(a.bundled_path()))
.map(|asset| self.asset_destination_path(asset))
.collect();

// The CLI creates a .version file in the asset dir to keep track of what version of the optimizer
Expand Down Expand Up @@ -1511,7 +1511,7 @@ impl BuildRequest {
// Queue the bundled assets
for bundled in assets.unique_assets() {
let from = PathBuf::from(bundled.absolute_source_path());
let to = asset_dir.join(bundled.bundled_path());
let to = self.asset_destination_path(bundled);

// prefer to log using a shorter path relative to the workspace dir by trimming the workspace dir
let from_ = from
Expand Down Expand Up @@ -4409,6 +4409,84 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
}
}

pub(crate) fn normalized_mount_path(&self, options: &AssetOptions) -> Option<PathBuf> {
let raw = options.mount_path()?;

let trimmed = raw.trim();
if trimmed.is_empty() {
return Some(PathBuf::new());
}

let slashes_trimmed = trimmed.trim_matches('/');
if slashes_trimmed.is_empty() {
return Some(PathBuf::new());
}

let candidate = PathBuf::from(slashes_trimmed);
if candidate
.components()
.any(|component| matches!(component, Component::ParentDir | Component::Prefix(_)))
{
tracing::warn!("Ignoring invalid asset mount path {raw:?}; falling back to /assets");
return None;
}

Some(candidate)
}

pub(crate) fn asset_destination_path(&self, bundled: &BundledAsset) -> PathBuf {
if let Some(mount) = self.normalized_mount_path(bundled.options()) {
let mut base = if self.bundle == BundleFormat::Web {
self.root_dir()
} else {
self.asset_dir()
};

if !mount.as_os_str().is_empty() {
base = base.join(&mount);
}

base.join(bundled.bundled_path())
} else {
self.asset_dir().join(bundled.bundled_path())
}
}

pub(crate) fn asset_public_path(&self, bundled: &BundledAsset) -> PathBuf {
let mount = self.normalized_mount_path(bundled.options());

if self.bundle == BundleFormat::Web {
let mut path = PathBuf::from("/");
if let Some(base) = self.base_path() {
let trimmed = base.trim_matches('/');
if !trimmed.is_empty() {
path = path.join(trimmed);
}
}

if let Some(ref mount_path) = mount {
if !mount_path.as_os_str().is_empty() {
path = path.join(mount_path);
}
} else {
path = path.join("assets");
}

return path.join(bundled.bundled_path());
}

let mut path = PathBuf::from("/");
if let Some(ref mount_path) = mount {
if !mount_path.as_os_str().is_empty() {
path = path.join(mount_path);
}
} else {
path = path.join("assets");
}

path.join(bundled.bundled_path())
}

/// The directory in which we'll put the main exe
///
/// Mac, Android, Web are a little weird
Expand Down Expand Up @@ -4777,26 +4855,27 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{

// Inject any resources from manganis into the head
for asset in assets.unique_assets() {
let asset_path = asset.bundled_path();
let public_path = self.asset_public_path(asset);
let public_path = public_path.to_string_lossy().replace('\\', "/");
match asset.options().variant() {
AssetVariant::Css(css_options) => {
if css_options.preloaded() {
head_resources.push_str(&format!(
"<link rel=\"preload\" as=\"style\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
"<link rel=\"preload\" as=\"style\" href=\"{public_path}\" crossorigin>"
))
}
}
AssetVariant::Image(image_options) => {
if image_options.preloaded() {
head_resources.push_str(&format!(
"<link rel=\"preload\" as=\"image\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
"<link rel=\"preload\" as=\"image\" href=\"{public_path}\" crossorigin>"
))
}
}
AssetVariant::Js(js_options) => {
if js_options.preloaded() {
head_resources.push_str(&format!(
"<link rel=\"preload\" as=\"script\" href=\"/{{base_path}}/assets/{asset_path}\" crossorigin>"
"<link rel=\"preload\" as=\"script\" href=\"{public_path}\" crossorigin>"
))
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/serve/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ impl AppServer {
// todo(jon): don't hardcode this here
if let Some(bundled_names) = self.client.hotreload_bundled_assets(path).await {
for bundled_name in bundled_names {
assets.push(PathBuf::from("/assets/").join(bundled_name));
assets.push(bundled_name);
}
}

Expand Down
44 changes: 27 additions & 17 deletions packages/manganis/manganis-core/src/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,25 +155,35 @@ impl Asset {
return PathBuf::from(self.bundled().absolute_source_path.as_str());
}

let mut bundle_root = PathBuf::from("/");

#[cfg(feature = "dioxus")]
let bundle_root = {
{
let base_path = dioxus_cli_config::base_path();
let base_path = base_path
.as_deref()
.map(|base_path| {
let trimmed = base_path.trim_matches('/');
format!("/{trimmed}")
})
.unwrap_or_default();
PathBuf::from(format!("{base_path}/assets/"))
};
#[cfg(not(feature = "dioxus"))]
let bundle_root = PathBuf::from("/assets/");

// Otherwise presumably we're bundled and we can use the bundled path
bundle_root.join(PathBuf::from(
self.bundled().bundled_path.as_str().trim_start_matches('/'),
))
if let Some(base) = base_path.as_deref() {
let trimmed = base.trim_matches('/');
if !trimmed.is_empty() {
bundle_root = bundle_root.join(trimmed);
}
}
}

let bundled = self.bundled();
let bundled_options = bundled.options();

match bundled_options.mount_path() {
Some(raw_mount) => {
let trimmed = raw_mount.trim_matches('/');
if !trimmed.is_empty() {
bundle_root = bundle_root.join(trimmed);
}
}
None => {
bundle_root = bundle_root.join("assets");
}
}

bundle_root.join(bundled.bundled_path().trim_start_matches('/'))
}
}

Expand Down
11 changes: 9 additions & 2 deletions packages/manganis/manganis-core/src/css.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{AssetOptions, AssetOptionsBuilder, AssetVariant};
use const_serialize::SerializeConst;
use const_serialize::{ConstStr, SerializeConst};

/// Options for a css asset
#[derive(
Expand Down Expand Up @@ -91,9 +91,16 @@ impl AssetOptionsBuilder<CssAssetOptions> {

/// Convert the options into options for a generic asset
pub const fn into_asset_options(self) -> AssetOptions {
let (mount_path, has_mount_path) = match self.mount_path {
Some(path) => (ConstStr::new(path), true),
None => (ConstStr::new(""), false),
};

AssetOptions {
add_hash: true,
add_hash: self.add_hash,
variant: AssetVariant::Css(self.variant),
mount_path,
has_mount_path,
}
}
}
9 changes: 8 additions & 1 deletion packages/manganis/manganis-core/src/css_module.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{AssetOptions, AssetOptionsBuilder, AssetVariant};
use const_serialize::SerializeConst;
use const_serialize::{ConstStr, SerializeConst};
use std::collections::HashSet;

/// Options for a css module asset
Expand Down Expand Up @@ -84,9 +84,16 @@ impl AssetOptionsBuilder<CssModuleAssetOptions> {

/// Convert the options into options for a generic asset
pub const fn into_asset_options(self) -> AssetOptions {
let (mount_path, has_mount_path) = match self.mount_path {
Some(path) => (ConstStr::new(path), true),
None => (ConstStr::new(""), false),
};

AssetOptions {
add_hash: self.add_hash,
variant: AssetVariant::CssModule(self.variant),
mount_path,
has_mount_path,
}
}
}
Expand Down
Loading
Loading