Skip to content

Commit

Permalink
Add liveview support to the CLI and make fullstack runnable from dist (
Browse files Browse the repository at this point in the history
…#2759)

* add liveview cli support
* Fix TUI fullstack deadlock
* look for fullstack assets in the public directory
* Fix fullstack with the CLI
* Fix static generation server
  • Loading branch information
ealmloff authored Aug 2, 2024
1 parent 9dbdf74 commit e5e578d
Show file tree
Hide file tree
Showing 14 changed files with 428 additions and 272 deletions.
7 changes: 7 additions & 0 deletions packages/cli-config/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ pub enum Platform {
#[cfg_attr(feature = "cli", clap(name = "static-generation"))]
#[serde(rename = "static-generation")]
StaticGeneration,

/// Targeting the static generation platform using SSR and Dioxus-Fullstack
#[cfg_attr(feature = "cli", clap(name = "liveview"))]
#[serde(rename = "liveview")]
Liveview,
}

/// An error that occurs when a platform is not recognized
Expand All @@ -50,6 +55,7 @@ impl FromStr for Platform {
"desktop" => Ok(Self::Desktop),
"fullstack" => Ok(Self::Fullstack),
"static-generation" => Ok(Self::StaticGeneration),
"liveview" => Ok(Self::Liveview),
_ => Err(UnknownPlatformError),
}
}
Expand Down Expand Up @@ -78,6 +84,7 @@ impl Platform {
Platform::Desktop => "desktop",
Platform::Fullstack => "fullstack",
Platform::StaticGeneration => "static-generation",
Platform::Liveview => "liveview",
}
}
}
Expand Down
86 changes: 47 additions & 39 deletions packages/cli/src/assets.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
use crate::builder::{
BuildMessage, MessageSource, MessageType, Stage, UpdateBuildProgress, UpdateStage,
BuildMessage, BuildRequest, MessageSource, MessageType, Stage, UpdateBuildProgress, UpdateStage,
};
use crate::dioxus_crate::DioxusCrate;
use crate::Result;
use anyhow::Context;
use brotli::enc::BrotliEncoderParams;
use futures_channel::mpsc::UnboundedSender;
use manganis_cli_support::{process_file, AssetManifest, AssetManifestExt, AssetType};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use std::fs;
use std::path::Path;
use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
use std::{ffi::OsString, path::PathBuf};
use std::{fs::File, io::Write};
use tracing::Level;
Expand All @@ -17,8 +19,8 @@ use walkdir::WalkDir;
/// The temp file name for passing manganis json from linker to current exec.
pub const MG_JSON_OUT: &str = "mg-out";

pub fn asset_manifest(config: &DioxusCrate) -> AssetManifest {
let file_path = config.out_dir().join(MG_JSON_OUT);
pub fn asset_manifest(build: &BuildRequest) -> AssetManifest {
let file_path = build.target_out_dir().join(MG_JSON_OUT);
let read = fs::read_to_string(&file_path).unwrap();
_ = fs::remove_file(file_path);
let json: Vec<String> = serde_json::from_str(&read).unwrap();
Expand All @@ -27,58 +29,64 @@ pub fn asset_manifest(config: &DioxusCrate) -> AssetManifest {
}

/// Create a head file that contains all of the imports for assets that the user project uses
pub fn create_assets_head(config: &DioxusCrate, manifest: &AssetManifest) -> Result<()> {
let mut file = File::create(config.out_dir().join("__assets_head.html"))?;
pub fn create_assets_head(build: &BuildRequest, manifest: &AssetManifest) -> Result<()> {
let out_dir = build.target_out_dir();
std::fs::create_dir_all(&out_dir)?;
let mut file = File::create(out_dir.join("__assets_head.html"))?;
file.write_all(manifest.head().as_bytes())?;
Ok(())
}

/// Process any assets collected from the binary
pub(crate) fn process_assets(
config: &DioxusCrate,
build: &BuildRequest,
manifest: &AssetManifest,
progress: &mut UnboundedSender<UpdateBuildProgress>,
) -> anyhow::Result<()> {
let static_asset_output_dir = config.out_dir();
let static_asset_output_dir = build.target_out_dir();

std::fs::create_dir_all(&static_asset_output_dir)
.context("Failed to create static asset output directory")?;

let mut assets_finished: usize = 0;
let assets_finished = Arc::new(AtomicUsize::new(0));
let assets = manifest.assets();
let asset_count = assets.len();
assets.iter().try_for_each(move |asset| {
if let AssetType::File(file_asset) = asset {
match process_file(file_asset, &static_asset_output_dir) {
Ok(_) => {
// Update the progress
_ = progress.start_send(UpdateBuildProgress {
stage: Stage::OptimizingAssets,
update: UpdateStage::AddMessage(BuildMessage {
level: Level::INFO,
message: MessageType::Text(format!(
"Optimized static asset {}",
file_asset
)),
source: MessageSource::Build,
}),
});
assets_finished += 1;
_ = progress.start_send(UpdateBuildProgress {
stage: Stage::OptimizingAssets,
update: UpdateStage::SetProgress(
assets_finished as f64 / asset_count as f64,
),
});
}
Err(err) => {
tracing::error!("Failed to copy static asset: {}", err);
return Err(err);
assets.par_iter().try_for_each_init(
|| progress.clone(),
move |progress, asset| {
if let AssetType::File(file_asset) = asset {
match process_file(file_asset, &static_asset_output_dir) {
Ok(_) => {
// Update the progress
_ = progress.start_send(UpdateBuildProgress {
stage: Stage::OptimizingAssets,
update: UpdateStage::AddMessage(BuildMessage {
level: Level::INFO,
message: MessageType::Text(format!(
"Optimized static asset {}",
file_asset
)),
source: MessageSource::Build,
}),
});
let assets_finished =
assets_finished.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
_ = progress.start_send(UpdateBuildProgress {
stage: Stage::OptimizingAssets,
update: UpdateStage::SetProgress(
assets_finished as f64 / asset_count as f64,
),
});
}
Err(err) => {
tracing::error!("Failed to copy static asset: {}", err);
return Err(err);
}
}
}
}
Ok::<(), anyhow::Error>(())
})?;
Ok::<(), anyhow::Error>(())
},
)?;

Ok(())
}
Expand Down
125 changes: 69 additions & 56 deletions packages/cli/src/builder/cargo.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::web::install_web_build_tooling;
use super::BuildRequest;
use super::BuildResult;
use super::TargetPlatform;
use crate::assets::copy_dir_to;
use crate::assets::create_assets_head;
use crate::assets::{asset_manifest, process_assets, AssetConfigDropGuard};
Expand All @@ -12,9 +13,12 @@ use crate::builder::progress::UpdateStage;
use crate::link::LinkCommand;
use crate::Result;
use anyhow::Context;
use dioxus_cli_config::Platform;
use futures_channel::mpsc::UnboundedSender;
use manganis_cli_support::AssetManifest;
use manganis_cli_support::ManganisSupportGuard;
use std::fs::create_dir_all;
use std::path::PathBuf;

impl BuildRequest {
/// Create a list of arguments for cargo builds
Expand All @@ -41,11 +45,10 @@ impl BuildRequest {
cargo_args.push(features_str);
}

if let Some(target) = self.web.then_some("wasm32-unknown-unknown").or(self
.build_arguments
.target_args
.target
.as_deref())
if let Some(target) = self
.targeting_web()
.then_some("wasm32-unknown-unknown")
.or(self.build_arguments.target_args.target.as_deref())
{
cargo_args.push("--target".to_string());
cargo_args.push(target.to_string());
Expand Down Expand Up @@ -94,7 +97,7 @@ impl BuildRequest {
Ok((cmd, cargo_args))
}

pub async fn build(
pub(crate) async fn build(
&self,
mut progress: UnboundedSender<UpdateBuildProgress>,
) -> Result<BuildResult> {
Expand All @@ -115,7 +118,7 @@ impl BuildRequest {
AssetConfigDropGuard::new(self.dioxus_crate.dioxus_config.web.app.base_path.as_deref());

// If this is a web, build make sure we have the web build tooling set up
if self.web {
if self.targeting_web() {
install_web_build_tooling(&mut progress).await?;
}

Expand All @@ -133,13 +136,8 @@ impl BuildRequest {
.context("Failed to post process build")?;

tracing::info!(
"🚩 Build completed: [./{}]",
self.dioxus_crate
.dioxus_config
.application
.out_dir
.clone()
.display()
"🚩 Build completed: [{}]",
self.dioxus_crate.out_dir().display()
);

_ = progress.start_send(UpdateBuildProgress {
Expand All @@ -161,30 +159,17 @@ impl BuildRequest {
update: UpdateStage::Start,
});

// Start Manganis linker intercept.
let linker_args = vec![format!("{}", self.dioxus_crate.out_dir().display())];

// Don't block the main thread - manganis should not be running its own std process but it's
// fine to wrap it here at the top
tokio::task::spawn_blocking(move || {
manganis_cli_support::start_linker_intercept(
&LinkCommand::command_name(),
cargo_args,
Some(linker_args),
)
})
.await
.unwrap()?;
let assets = self.collect_assets(cargo_args, progress).await?;

let file_name = self.dioxus_crate.executable_name();

// Move the final output executable into the dist folder
let out_dir = self.dioxus_crate.out_dir();
let out_dir = self.target_out_dir();
if !out_dir.is_dir() {
create_dir_all(&out_dir)?;
}
let mut output_path = out_dir.join(file_name);
if self.web {
if self.targeting_web() {
output_path.set_extension("wasm");
} else if cfg!(windows) {
output_path.set_extension("exe");
Expand All @@ -195,52 +180,68 @@ impl BuildRequest {

self.copy_assets_dir()?;

let assets = if !self.build_arguments.skip_assets {
let assets = asset_manifest(&self.dioxus_crate);
let dioxus_crate = self.dioxus_crate.clone();
let mut progress = progress.clone();
tokio::task::spawn_blocking(
move || -> Result<Option<manganis_cli_support::AssetManifest>> {
// Collect assets
process_assets(&dioxus_crate, &assets, &mut progress)?;
// Create the __assets_head.html file for bundling
create_assets_head(&dioxus_crate, &assets)?;
Ok(Some(assets))
},
)
.await
.unwrap()?
} else {
None
};

// Create the build result
let build_result = BuildResult {
executable: output_path,
web: self.web,
platform: self
.build_arguments
.platform
.expect("To be resolved by now"),
target_platform: self.target_platform,
};

// If this is a web build, run web post processing steps
if self.web {
if self.targeting_web() {
self.post_process_web_build(&build_result, assets.as_ref(), progress)
.await?;
}

Ok(build_result)
}

async fn collect_assets(
&self,
cargo_args: Vec<String>,
progress: &mut UnboundedSender<UpdateBuildProgress>,
) -> anyhow::Result<Option<AssetManifest>> {
// If this is the server build, the client build already copied any assets we need
if self.target_platform == TargetPlatform::Server {
return Ok(None);
}
// If assets are skipped, we don't need to collect them
if self.build_arguments.skip_assets {
return Ok(None);
}

// Start Manganis linker intercept.
let linker_args = vec![format!("{}", self.target_out_dir().display())];

// Don't block the main thread - manganis should not be running its own std process but it's
// fine to wrap it here at the top
let build = self.clone();
let mut progress = progress.clone();
tokio::task::spawn_blocking(move || {
manganis_cli_support::start_linker_intercept(
&LinkCommand::command_name(),
cargo_args,
Some(linker_args),
)?;
let assets = asset_manifest(&build);
// Collect assets from the asset manifest the linker intercept created
process_assets(&build, &assets, &mut progress)?;
// Create the __assets_head.html file for bundling
create_assets_head(&build, &assets)?;

Ok(Some(assets))
})
.await
.unwrap()
}

pub fn copy_assets_dir(&self) -> anyhow::Result<()> {
tracing::info!("Copying public assets to the output directory...");
let out_dir = self.dioxus_crate.out_dir();
let asset_dir = self.dioxus_crate.asset_dir();

if asset_dir.is_dir() {
// Only pre-compress the assets from the web build. Desktop assets are not served, so they don't need to be pre_compressed
let pre_compress = self.web
let pre_compress = self.targeting_web()
&& self
.dioxus_crate
.should_pre_compress_web_assets(self.build_arguments.release);
Expand All @@ -249,4 +250,16 @@ impl BuildRequest {
}
Ok(())
}

/// Get the output directory for a specific built target
pub fn target_out_dir(&self) -> PathBuf {
let out_dir = self.dioxus_crate.out_dir();
match self.build_arguments.platform {
Some(Platform::Fullstack | Platform::StaticGeneration) => match self.target_platform {
TargetPlatform::Web => out_dir.join("public"),
_ => out_dir,
},
_ => out_dir,
}
}
}
Loading

0 comments on commit e5e578d

Please sign in to comment.