Support public dir (#4783)
* Add optional static_dir support - add optional application.static_dir field defaulting to none - copy configured static directory into web output during build/serve - document config knob and add unit tests - force full rebuild when static_dir contents change - sync removes stale outputs and logs copy activity - track static manifest next to platform dir Tests: cargo fmt * default to public dir to match vite * static_dir -> public_dir * wip * deser as none... * wip... use .manifest.json * implement dumb form of asset manifest generation * implement manifest using asset system directly * cleanup * dont create extra asset dir for server --------- Co-authored-by: Evan Almloff <evanalmloff@gmail.com> Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
10 files changed
+245
-78
No files matched your search
@@ -6,8 +6,8 @@ name = "project_name"
|
||||
# `build` & `serve` output path
|
||||
out_dir = "dist"
|
||||
|
||||
# The static resource path
|
||||
asset_dir = "public"
|
||||
# Static files copied verbatim into the built app (set to enable, e.g. "public")
|
||||
public_dir = "public"
|
||||
|
||||
[web.app]
|
||||
|
||||
|
||||
@@ -60,9 +60,11 @@ name = "project-name"
|
||||
# Currently supported platforms: web, desktop
|
||||
default_platform = "web"
|
||||
|
||||
# Optional: enable copying from a static directory (e.g. "public")
|
||||
public_dir = "public"
|
||||
|
||||
[web.app]
|
||||
title = "Hello"
|
||||
|
||||
[web.resource.dev]
|
||||
```
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
//! The build manifest for `dx` applications, containing metadata about the build including
|
||||
//! the CLI version, Rust version, and all bundled assets.
|
||||
//!
|
||||
//! We eventually plan to use this manifest to support tighter integration with deployment platforms
|
||||
//! and CDNs.
|
||||
//!
|
||||
//! This manifest contains the list of assets, rust version, and cli version used to build the app.
|
||||
//! Eventually, we might want to expand this to include more metadata about the build, including
|
||||
//! build time, target platform, etc.
|
||||
|
||||
use dioxus_cli_opt::AssetManifest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct AppManifest {
|
||||
/// Stable since 0.7.0
|
||||
pub cli_version: String,
|
||||
|
||||
/// Stable since 0.7.0
|
||||
pub rust_version: String,
|
||||
|
||||
/// Stable since 0.7.0
|
||||
pub assets: AssetManifest,
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
mod assets;
|
||||
mod builder;
|
||||
mod context;
|
||||
mod manifest;
|
||||
mod patch;
|
||||
mod pre_render;
|
||||
mod request;
|
||||
@@ -19,6 +20,7 @@ mod tools;
|
||||
pub(crate) use assets::*;
|
||||
pub(crate) use builder::*;
|
||||
pub(crate) use context::*;
|
||||
pub(crate) use manifest::*;
|
||||
pub(crate) use patch::*;
|
||||
pub(crate) use pre_render::*;
|
||||
pub(crate) use request::*;
|
||||
|
||||
@@ -320,9 +320,9 @@
|
||||
//! - xbuild: <https://github.com/rust-mobile/xbuild/blob/master/xbuild/src/command/build.rs>
|
||||
|
||||
use crate::{
|
||||
AndroidTools, BuildContext, BuildId, BundleFormat, DioxusConfig, Error, LinkAction,
|
||||
LinkerFlavor, Platform, Renderer, Result, RustcArgs, TargetArgs, TraceSrc, WasmBindgen,
|
||||
WasmOptConfig, Workspace, DX_RUSTC_WRAPPER_ENV_VAR,
|
||||
AndroidTools, AppManifest, BuildContext, BuildId, BundleFormat, DioxusConfig, Error,
|
||||
LinkAction, LinkerFlavor, Platform, Renderer, Result, RustcArgs, TargetArgs, TraceSrc,
|
||||
WasmBindgen, WasmOptConfig, Workspace, DX_RUSTC_WRAPPER_ENV_VAR,
|
||||
};
|
||||
use anyhow::{bail, Context};
|
||||
use cargo_metadata::diagnostic::Diagnostic;
|
||||
@@ -333,8 +333,8 @@ 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_core::AssetVariant;
|
||||
use manganis::{AssetOptions, BundledAsset};
|
||||
use manganis_core::{AssetOptionsBuilder, AssetVariant};
|
||||
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{borrow::Cow, ffi::OsString};
|
||||
@@ -1295,13 +1295,40 @@ impl BuildRequest {
|
||||
/// Collect the assets from the final executable and modify the binary in place to point to the right
|
||||
/// hashed asset location.
|
||||
async fn collect_assets(&self, exe: &Path, ctx: &BuildContext) -> Result<AssetManifest> {
|
||||
// walk every file in the incremental cache dir, reading and inserting items into the manifest.
|
||||
let mut manifest = AssetManifest::default();
|
||||
|
||||
// And then add from the exe directly, just in case it's LTO compiled and has no incremental cache
|
||||
if !self.skip_assets {
|
||||
ctx.status_extracting_assets();
|
||||
manifest = super::assets::extract_assets_from_file(exe).await?;
|
||||
if self.skip_assets {
|
||||
return Ok(AssetManifest::default());
|
||||
}
|
||||
|
||||
ctx.status_extracting_assets();
|
||||
|
||||
let mut manifest = super::assets::extract_assets_from_file(exe).await?;
|
||||
|
||||
// If the user has a public dir, we submit all the entries there as assets too
|
||||
//
|
||||
// These don't receive a hash in their filename, since they're user-provided static assets
|
||||
// We only do this for web builds
|
||||
if matches!(self.bundle, BundleFormat::Web)
|
||||
&& matches!(ctx.mode, BuildMode::Base { .. } | BuildMode::Fat)
|
||||
{
|
||||
if let Some(dir) = self.user_public_dir() {
|
||||
for entry in walkdir::WalkDir::new(&dir)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.file_type().is_file())
|
||||
{
|
||||
let from = entry.path().to_path_buf();
|
||||
let relative_path = from.strip_prefix(&dir).unwrap();
|
||||
let to = format!("../{}", relative_path.display());
|
||||
manifest.insert_asset(BundledAsset::new(
|
||||
from.to_string_lossy().as_ref(),
|
||||
to.as_str(),
|
||||
AssetOptionsBuilder::new()
|
||||
.with_hash_suffix(false)
|
||||
.into_asset_options(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(manifest)
|
||||
@@ -1494,14 +1521,13 @@ impl BuildRequest {
|
||||
.map(|a| asset_dir.join(a.bundled_path()))
|
||||
.collect();
|
||||
|
||||
// The CLI creates a .version file in the asset dir to keep track of what version of the optimizer
|
||||
// the asset was processed. If that version doesn't match the CLI version, we need to re-optimize
|
||||
// all assets.
|
||||
let version_file = self.asset_optimizer_version_file();
|
||||
let clear_cache = std::fs::read_to_string(&version_file)
|
||||
.ok()
|
||||
.filter(|s| s == crate::VERSION.as_str())
|
||||
.is_none();
|
||||
// The CLI creates a .manifest.json file in the asset dir to keep track of the assets and
|
||||
// other build metadata. If we can't parse this file (or the CLI version changed), then we
|
||||
// want to re-copy all the assets rather than trying to do an incremental update.
|
||||
let clear_cache = self
|
||||
.load_manifest()
|
||||
.map(|manifest| manifest.cli_version != crate::VERSION.as_str())
|
||||
.unwrap_or(true);
|
||||
if clear_cache {
|
||||
keep_bundled_output_paths.clear();
|
||||
}
|
||||
@@ -1511,20 +1537,6 @@ impl BuildRequest {
|
||||
keep_bundled_output_paths
|
||||
);
|
||||
|
||||
// use walkdir::WalkDir;
|
||||
// for item in WalkDir::new(&asset_dir).into_iter().flatten() {
|
||||
// // If this asset is in the manifest, we don't need to remove it
|
||||
// let canonicalized = dunce::canonicalize(item.path())?;
|
||||
// if !keep_bundled_output_paths.contains(canonicalized.as_path()) {
|
||||
// // Remove empty dirs, remove files not in the manifest
|
||||
// if item.file_type().is_dir() && item.path().read_dir()?.next().is_none() {
|
||||
// std::fs::remove_dir(item.path())?;
|
||||
// } else {
|
||||
// std::fs::remove_file(item.path())?;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// todo(jon): we also want to eventually include options for each asset's optimization and compression, which we currently aren't
|
||||
let mut assets_to_transfer = vec![];
|
||||
|
||||
@@ -1589,7 +1601,7 @@ impl BuildRequest {
|
||||
}
|
||||
|
||||
// Write the version file so we know what version of the optimizer we used
|
||||
std::fs::write(self.asset_optimizer_version_file(), crate::VERSION.as_str())?;
|
||||
self.write_app_manifest(assets).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -3116,8 +3128,18 @@ impl BuildRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a workdir for the given platform
|
||||
/// This can be used as a temporary directory for the build, but in an observable way such that
|
||||
/// you can see the files in the directory via `target`
|
||||
///
|
||||
/// target/dx/build/app/web/
|
||||
/// target/dx/build/app/web/public/
|
||||
/// target/dx/build/app/web/server.exe
|
||||
fn platform_dir(&self) -> PathBuf {
|
||||
self.build_dir(self.bundle, self.release)
|
||||
self.internal_out_dir()
|
||||
.join(&self.main_target)
|
||||
.join(if self.release { "release" } else { "debug" })
|
||||
.join(self.bundle.build_folder_name())
|
||||
}
|
||||
|
||||
fn platform_exe_name(&self) -> String {
|
||||
@@ -3416,20 +3438,6 @@ impl BuildRequest {
|
||||
dir
|
||||
}
|
||||
|
||||
/// Create a workdir for the given platform
|
||||
/// This can be used as a temporary directory for the build, but in an observable way such that
|
||||
/// you can see the files in the directory via `target`
|
||||
///
|
||||
/// target/dx/build/app/web/
|
||||
/// target/dx/build/app/web/public/
|
||||
/// target/dx/build/app/web/server.exe
|
||||
pub(crate) fn build_dir(&self, bundle: BundleFormat, release: bool) -> PathBuf {
|
||||
self.internal_out_dir()
|
||||
.join(&self.main_target)
|
||||
.join(if release { "release" } else { "debug" })
|
||||
.join(bundle.build_folder_name())
|
||||
}
|
||||
|
||||
/// target/dx/bundle/app/
|
||||
/// target/dx/bundle/app/blah.app
|
||||
/// target/dx/bundle/app/blah.exe
|
||||
@@ -4422,12 +4430,13 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
|
||||
.join("main")
|
||||
.join("assets"),
|
||||
|
||||
// We put assets in public/assets for server apps
|
||||
BundleFormat::Server => self.root_dir().join("public").join("assets"),
|
||||
|
||||
// everyone else is soooo normal, just app/assets :)
|
||||
BundleFormat::Web
|
||||
| BundleFormat::Ios
|
||||
| BundleFormat::Windows
|
||||
| BundleFormat::Linux
|
||||
| BundleFormat::Server => self.root_dir().join("assets"),
|
||||
BundleFormat::Web | BundleFormat::Ios | BundleFormat::Windows | BundleFormat::Linux => {
|
||||
self.root_dir().join("assets")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4482,9 +4491,21 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
|
||||
.with_extension("wasm")
|
||||
}
|
||||
|
||||
/// Get the path to the asset optimizer version file
|
||||
pub(crate) fn asset_optimizer_version_file(&self) -> PathBuf {
|
||||
self.platform_dir().join(".cli-version")
|
||||
/// Get the path to the app manifest file
|
||||
///
|
||||
/// This includes metadata about the build such as the bundle format, target triple, features, etc.
|
||||
/// Manifests are only written by the `PRIMARY` build.
|
||||
pub(crate) fn app_manifest(&self) -> PathBuf {
|
||||
self.platform_dir().join(".manifest.json")
|
||||
}
|
||||
|
||||
pub(crate) fn load_manifest(&self) -> Result<AppManifest> {
|
||||
let manifest_path = self.app_manifest();
|
||||
let manifest_data = std::fs::read_to_string(&manifest_path)
|
||||
.with_context(|| format!("Failed to read manifest at {:?}", &manifest_path))?;
|
||||
let manifest: AppManifest = serde_json::from_str(&manifest_data)
|
||||
.with_context(|| format!("Failed to parse manifest at {:?}", &manifest_path))?;
|
||||
Ok(manifest)
|
||||
}
|
||||
|
||||
/// Check for tooling that might be required for this build.
|
||||
@@ -4882,6 +4903,34 @@ __wbg_init({{module_or_path: "/{}/{wasm_path}"}}).then((wasm) => {{
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the configured public directory relative to the crate, if any.
|
||||
pub(crate) fn user_public_dir(&self) -> Option<PathBuf> {
|
||||
let path = self.config.application.public_dir.as_ref()?;
|
||||
|
||||
if path.as_os_str().is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(if path.is_absolute() {
|
||||
path.clone()
|
||||
} else {
|
||||
self.crate_dir().join(path)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn path_is_in_public_dir(&self, path: &Path) -> bool {
|
||||
let Some(static_dir) = self.user_public_dir() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Canonicalize when possible so we work with editors that use tmp files
|
||||
let canonical_static =
|
||||
dunce::canonicalize(&static_dir).unwrap_or_else(|_| static_dir.clone());
|
||||
let canonical_path = dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
|
||||
|
||||
canonical_path.starts_with(&canonical_static)
|
||||
}
|
||||
|
||||
/// Get the base path from the config or None if this is not a web or server build
|
||||
pub(crate) fn base_path(&self) -> Option<&str> {
|
||||
self.base_path
|
||||
@@ -5289,4 +5338,17 @@ We checked the folders:
|
||||
TEAM_IDENTIFIER = mbfile.team_identifier[0],
|
||||
))
|
||||
}
|
||||
|
||||
async fn write_app_manifest(&self, assets: &AssetManifest) -> Result<()> {
|
||||
let manifest = AppManifest {
|
||||
assets: assets.clone(),
|
||||
cli_version: crate::VERSION.to_string(),
|
||||
rust_version: self.workspace.rustc_version.clone(),
|
||||
};
|
||||
|
||||
let manifest_path = self.app_manifest();
|
||||
std::fs::write(&manifest_path, serde_json::to_string_pretty(&manifest)?)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,10 @@ pub(crate) struct ApplicationConfig {
|
||||
#[serde(default)]
|
||||
pub(crate) out_dir: Option<PathBuf>,
|
||||
|
||||
#[serde(default = "public_dir_default")]
|
||||
#[serde(deserialize_with = "empty_string_is_none")]
|
||||
pub(crate) public_dir: Option<PathBuf>,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) tailwind_input: Option<PathBuf>,
|
||||
|
||||
@@ -43,3 +47,19 @@ pub(crate) struct ApplicationConfig {
|
||||
#[serde(default)]
|
||||
pub(crate) android_min_sdk_version: Option<u32>,
|
||||
}
|
||||
|
||||
fn public_dir_default() -> Option<PathBuf> {
|
||||
Some("public".into())
|
||||
}
|
||||
|
||||
fn empty_string_is_none<'de, D>(deserializer: D) -> Result<Option<PathBuf>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let opt: Option<String> = Option::deserialize(deserializer)?;
|
||||
match opt {
|
||||
Some(s) if s.is_empty() => Ok(None),
|
||||
Some(s) => Ok(Some(PathBuf::from(s))),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ impl Default for DioxusConfig {
|
||||
Self {
|
||||
application: ApplicationConfig {
|
||||
out_dir: None,
|
||||
public_dir: Some("public".into()),
|
||||
tailwind_input: None,
|
||||
tailwind_output: None,
|
||||
ios_info_plist: None,
|
||||
@@ -55,3 +56,42 @@ impl Default for DioxusConfig {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn static_dir_defaults_to_public() {
|
||||
let config = DioxusConfig::default();
|
||||
assert_eq!(
|
||||
config.application.public_dir,
|
||||
Some(std::path::PathBuf::from("public"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_dir_can_be_overridden() {
|
||||
let source = r#"
|
||||
[application]
|
||||
public_dir = "public2"
|
||||
"#;
|
||||
|
||||
let config: DioxusConfig = toml::from_str(source).expect("parse config");
|
||||
assert_eq!(
|
||||
config.application.public_dir.as_deref(),
|
||||
Some(std::path::Path::new("public2"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_dir_can_be_disabled() {
|
||||
let source = r#"
|
||||
[application]
|
||||
public_dir = ""
|
||||
"#;
|
||||
|
||||
let config: DioxusConfig = toml::from_str(source).expect("parse config");
|
||||
assert_eq!(config.application.public_dir.as_deref(), None);
|
||||
}
|
||||
}
|
||||
@@ -379,6 +379,12 @@ impl AppServer {
|
||||
}
|
||||
}
|
||||
|
||||
// If it's in the public dir, we sync it and trigger a full rebuild
|
||||
if self.client.build.path_is_in_public_dir(path) {
|
||||
needs_full_rebuild = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If it's a rust file, we want to hotreload it using the filemap
|
||||
if ext == "rs" {
|
||||
// And grabout the contents
|
||||
@@ -1028,6 +1034,15 @@ impl AppServer {
|
||||
let mut watched_crates = self.local_dependencies(crate_package);
|
||||
watched_crates.push(crate_dir);
|
||||
|
||||
// Watch the `public` directory if this is the client crate
|
||||
if self.client.build.crate_package == crate_package {
|
||||
if let Some(public_dir) = self.client.build.user_public_dir() {
|
||||
if public_dir.exists() {
|
||||
watched_paths.push(public_dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now, watch all the folders in the crates, but respecting their respective ignore files
|
||||
for krate_root in watched_crates {
|
||||
// Build the ignore builder for this crate, but with our default ignore list as well
|
||||
|
||||
Reference in new issue
Block a user