Fix bundling/path errors, sidecar support, iOS/Android simulator support, asset hotreload fixes, serverfn hotrelad, native tailwind support (#2779)

* wip: fix manganis import path
* upgrade tauri bundler, convert todo!() to unimplemented!()
* feat: mobile hotreloading + ios asset configuration
* remove oid demo and its associated env vars
* nuke all the things that cause cache thrashing
* swap to Asset type
* add some more logs
* display impl for attrvalue
* dont panic on collect failure
* cut down web deps to 150
* clean up deps in a lot of places, simplify build scripts
* clean up asset resolution and cli-dev profile
* wire up absolute paths for manganis asset in a particular mode
* move document related things around to shorten compile times
* move most things to `document::Item` and then decompose the cli-config
* switch link to stylesheet for clarity in examples
* move manganis workspace example to examples folder
* decompose manganis-cli-support
* dont need mobile demo anymore
* remove gloo dialogs
* html doesnt need document
* rename hotreload to devtools
* really clean up the html crate
* fix weird rsx spacing
* clean up desktop to use callback
* clean up document a bit
* re-wire up devsocket
* fix utf css
* yeeeet that js out of here
* synthetic web system works
* web crate almost done being cleaned up
* desktop mostly cleaned up too
* clean up cli a bit
* more cli cleanup
* cli builds again
* clean up cli, inline structs, cut down number of unique types where possible
* hotreload works again for desktop and this time, mobile too
* cfg out tungstenite
* devserver err
* more refactor to cli builder
* switch to unbounded_send
* new structure is much cleaner
* add http serve subcommand
* bundled hotreload
* kick stylsheets
* clean up a bit more, split up eventloop
* amazingly serve seems done
* change from pub to pub(crate) in cli
* remove tools
* bit more polish to cli
* fix issue with join
* gracefully handle fullstack without a server
* fullstack mobile demo
* fix launch function, move projects into example-projects folder
* hoist examples
* add "run" command
* clean up launch
* remove old manual websocket receiver
* doctor command
* allow desktop to scroll
* cut apart router crate
* dont put launch in prelude
* use dioxus::launch where possible
* rename rsx, cut out hotreload tests
* remove liveview project
* bump native
* fix compile for renderers
* move sync event response out of interpreter
* move render in serve
* rollback settings change
* cli compiles, huzzah!
* change uris for asset
* fix asset
* new tui screen
* new cargo-like tui works
* very very verty close
* it works! very small bug with incorrect grapheme calc
* Clean up devserver a bit
* status sytem
* tidy up debug filters
* clean up logging situation
* Fix a number of bugs with log printing
* new printing system is more reliable
* wire up more stuff
* things working but fullstack is having issues
* fullstack works again!
* hotreloading bundled assets works again
* bundled hotreload and beginnings of macos bundling
* Hotreload desktop
* combined server builds
* add build handle
* fix fullstack assets
* make open async, add some hooks for ios
* migrate filemap to runner
* wip global crate system
* fixup bundles + organize asset
* fix asset location bug
* all the bundled reloading!
* open ios simulator!
* full hotreload support for mobile + serverfn
* basic cleanups
* clean up dx
* Move filemap
* fix cutting of newlines
* assets workibg, some android
* hoist wry/tao
* use sync locks and headers to fix issues with android
* desktop -> mac/win/linux with alias
* better logging
* feat: workspace (entire computer!) hotreload
* should rebuild toggle, ios simulator bootup
* proper mobile support in launch
* more robust handling of assets
* fix cargo
* bring back some of tauri bundle
* make warnings go away, clippy happy on cli
* some final clippy cleanups
* fmt
* move manganis to its own folder
* upgrade bundle to stable
* drastically slim down manganis, prepping for merge
* typos, failing test, docsrs config
* remove static gen test
* nix static gen test
* we use --platform web instead of --platform fullstack now
* only bind dev urls in desktop/mobile
* install gtk
* nix static gen
* split build dir by app name
This commit is contained in:
Jonathan Kelley authored and GitHub committed 2024-10-25 17:23:45 -07:00
1 parent ef436e4ed0
commit 7ec3453ca3
156 files changed
+11544 -9005

No files matched your search

+312
View File
@@ -0,0 +1,312 @@
use super::{AppHandle, ServeUpdate, WebServer};
use crate::{
AppBundle, DioxusCrate, HotreloadFilemap, HotreloadResult, Platform, Result, TraceSrc,
};
use dioxus_core::internal::TemplateGlobalKey;
use dioxus_devtools_types::HotReloadMsg;
use dioxus_html::HtmlCtx;
use futures_util::{future::OptionFuture, stream::FuturesUnordered};
use ignore::gitignore::Gitignore;
use std::{
collections::{HashMap, HashSet},
net::SocketAddr,
path::PathBuf,
};
use tokio_stream::StreamExt;
pub(crate) struct AppRunner {
pub(crate) running: HashMap<Platform, AppHandle>,
pub(crate) krate: DioxusCrate,
pub(crate) file_map: HotreloadFilemap,
pub(crate) ignore: Gitignore,
pub(crate) applied_hot_reload_message: HotReloadMsg,
pub(crate) builds_opened: usize,
pub(crate) should_full_rebuild: bool,
}
impl AppRunner {
/// Create the AppRunner and then initialize the filemap with the crate directory.
pub(crate) fn start(krate: &DioxusCrate) -> Self {
let mut runner = Self {
running: Default::default(),
file_map: HotreloadFilemap::new(),
applied_hot_reload_message: Default::default(),
ignore: krate.workspace_gitignore(),
krate: krate.clone(),
builds_opened: 0,
should_full_rebuild: true,
};
// todo(jon): this might take a while so we should try and background it, or make it lazy somehow
// we could spawn a thread to search the FS and then when it returns we can fill the filemap
// in testing, if this hits a massive directory, it might take several seconds with no feedback.
for krate in krate.all_watched_crates() {
runner.fill_filemap(krate);
}
runner
}
pub(crate) async fn wait(&mut self) -> ServeUpdate {
// If there are no running apps, we can just return pending to avoid deadlocking
if self.running.is_empty() {
return futures_util::future::pending().await;
}
self.running
.iter_mut()
.map(|(platform, handle)| async {
use ServeUpdate::*;
let platform = *platform;
tokio::select! {
Some(Ok(Some(msg))) = OptionFuture::from(handle.app_stdout.as_mut().map(|f| f.next_line())) => {
StdoutReceived { platform, msg }
},
Some(Ok(Some(msg))) = OptionFuture::from(handle.app_stderr.as_mut().map(|f| f.next_line())) => {
StderrReceived { platform, msg }
},
Some(status) = OptionFuture::from(handle.app_child.as_mut().map(|f| f.wait())) => {
match status {
Ok(status) => ProcessExited { status, platform },
Err(_err) => todo!("handle error in process joining?"),
}
}
Some(Ok(Some(msg))) = OptionFuture::from(handle.server_stdout.as_mut().map(|f| f.next_line())) => {
StdoutReceived { platform: Platform::Server, msg }
},
Some(Ok(Some(msg))) = OptionFuture::from(handle.server_stderr.as_mut().map(|f| f.next_line())) => {
StderrReceived { platform: Platform::Server, msg }
},
Some(status) = OptionFuture::from(handle.server_child.as_mut().map(|f| f.wait())) => {
match status {
Ok(status) => ProcessExited { status, platform: Platform::Server },
Err(_err) => todo!("handle error in process joining?"),
}
}
else => futures_util::future::pending().await
}
})
.collect::<FuturesUnordered<_>>()
.next()
.await
.expect("Stream to pending if not empty")
}
/// Finally "bundle" this app and return a handle to it
pub(crate) async fn open(
&mut self,
app: AppBundle,
devserver_ip: SocketAddr,
fullstack_address: Option<SocketAddr>,
should_open_web: bool,
) -> Result<&AppHandle> {
let platform = app.build.build.platform();
// Drop the old handle
// todo(jon): we should instead be sending the kill signal rather than dropping the process
// This would allow a more graceful shutdown and fix bugs like desktop not retaining its size
self.kill(platform);
// wait a tiny sec for the processes to die so we don't have fullstack servers on top of each other
// todo(jon): we should allow rebinding to the same port in fullstack itself
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
// Start the new app before we kill the old one to give it a little bit of time
let mut handle = AppHandle::new(app).await?;
handle
.open(
devserver_ip,
fullstack_address,
self.builds_opened == 0 && should_open_web,
)
.await?;
self.builds_opened += 1;
self.running.insert(platform, handle);
Ok(self.running.get(&platform).unwrap())
}
pub(crate) fn kill(&mut self, platform: Platform) {
self.running.remove(&platform);
}
/// Open an existing app bundle, if it exists
pub(crate) async fn open_existing(&self, devserver: &WebServer) {
if let Some(address) = devserver.server_address() {
let url = format!("http://{address}");
tracing::debug!("opening url: {url}");
_ = open::that(url);
}
}
pub(crate) fn attempt_hot_reload(
&mut self,
modified_files: Vec<PathBuf>,
) -> Option<HotReloadMsg> {
// If we have any changes to the rust files, we need to update the file map
let mut templates = vec![];
// Prepare the hotreload message we need to send
let mut edited_rust_files = Vec::new();
let mut assets = Vec::new();
for path in modified_files {
// for various assets that might be linked in, we just try to hotreloading them forcefully
// That is, unless they appear in an include! macro, in which case we need to a full rebuild....
let Some(ext) = path.extension().and_then(|v| v.to_str()) else {
continue;
};
// If it's a rust file, we want to hotreload it using the filemap
if ext == "rs" {
edited_rust_files.push(path);
continue;
}
// Otherwise, it might be an asset and we should look for it in all the running apps
for runner in self.running.values() {
if let Some(bundled_name) = runner.hotreload_bundled_asset(&path) {
// todo(jon): don't hardcode this here
let asset_relative = PathBuf::from("/assets/").join(bundled_name);
assets.push(asset_relative);
}
}
}
// Multiple runners might have queued the same asset, so dedup them
assets.dedup();
// Process the rust files
for rust_file in edited_rust_files {
// Strip the prefix before sending it to the filemap
let Ok(path) = rust_file.strip_prefix(self.krate.workspace_dir()) else {
tracing::error!(
"Hotreloading file outside of the crate directory: {:?}",
rust_file
);
continue;
};
// And grabout the contents
let contents = std::fs::read_to_string(&rust_file).unwrap();
match self.file_map.update_rsx::<HtmlCtx>(path, contents) {
HotreloadResult::Rsx(new) => templates.extend(new),
// The rust file may have failed to parse, but that is most likely
// because the user is in the middle of adding new code
// We just ignore the error and let Rust analyzer warn about the problem
HotreloadResult::Notreloadable => return None,
HotreloadResult::NotParseable => {
tracing::debug!(dx_src = ?TraceSrc::Dev, "Error hotreloading file - not parseable {rust_file:?}")
}
}
}
let msg = HotReloadMsg {
templates,
assets,
unknown_files: vec![],
};
self.add_hot_reload_message(&msg);
Some(msg)
}
/// Get any hot reload changes that have been applied since the last full rebuild
pub(crate) fn applied_hot_reload_changes(&mut self) -> HotReloadMsg {
self.applied_hot_reload_message.clone()
}
/// Clear the hot reload changes. This should be called any time a new build is starting
pub(crate) fn clear_hot_reload_changes(&mut self) {
self.applied_hot_reload_message = Default::default();
}
/// Store the hot reload changes for any future clients that connect
fn add_hot_reload_message(&mut self, msg: &HotReloadMsg) {
let applied = &mut self.applied_hot_reload_message;
// Merge the assets, unknown files, and templates
// We keep the newer change if there is both a old and new change
let mut templates: HashMap<TemplateGlobalKey, _> = std::mem::take(&mut applied.templates)
.into_iter()
.map(|template| (template.key.clone(), template))
.collect();
let mut assets: HashSet<PathBuf> =
std::mem::take(&mut applied.assets).into_iter().collect();
let mut unknown_files: HashSet<PathBuf> = std::mem::take(&mut applied.unknown_files)
.into_iter()
.collect();
for template in &msg.templates {
templates.insert(template.key.clone(), template.clone());
}
assets.extend(msg.assets.iter().cloned());
unknown_files.extend(msg.unknown_files.iter().cloned());
applied.templates = templates.into_values().collect();
applied.assets = assets.into_iter().collect();
applied.unknown_files = unknown_files.into_iter().collect();
}
pub(crate) async fn client_connected(&mut self) {
for (platform, runner) in self.running.iter_mut() {
// Assign the runtime asset dir to the runner
if *platform == Platform::Ios {
// xcrun simctl get_app_container booted com.dioxuslabs
let res = tokio::process::Command::new("xcrun")
.arg("simctl")
.arg("get_app_container")
.arg("booted")
.arg("com.dioxuslabs")
.output()
.await;
if let Ok(res) = res {
tracing::debug!("Using runtime asset dir: {:?}", res);
if let Ok(out) = String::from_utf8(res.stdout) {
let out = out.trim();
tracing::debug!("Setting Runtime asset dir: {out:?}");
runner.runtime_asst_dir = Some(PathBuf::from(out));
}
}
}
}
}
/// Fill the filemap with files from the filesystem, using the given filter to determine which files to include.
///
/// You can use the filter with something like a gitignore to only include files that are relevant to your project.
/// We'll walk the filesystem from the given path and recursively search for all files that match the filter.
///
/// The filter function takes a path and returns true if the file should be included in the filemap.
/// Generally this will only be .rs files
///
/// If a file couldn't be parsed, we don't fail. Instead, we save the error.
pub fn fill_filemap(&mut self, path: PathBuf) {
if self.ignore.matched(&path, path.is_dir()).is_ignore() {
return;
}
// If the file is a .rs file, add it to the filemap
if path.extension().and_then(|s| s.to_str()) == Some("rs") {
if let Ok(contents) = std::fs::read_to_string(&path) {
if let Ok(path) = path.strip_prefix(self.krate.workspace_dir()) {
self.file_map.add_file(path.to_path_buf(), contents);
}
}
return;
}
// If it's not, we'll try to read the directory
if path.is_dir() {
if let Ok(read_dir) = std::fs::read_dir(&path) {
for entry in read_dir.flatten() {
self.fill_filemap(entry.path());
}
}
}
}
}