Merge branch 'master' into feature/use-shared-state-better-diagnostics

This commit is contained in:
Jonathan Kelley authored and GitHub committed 2023-07-20 10:51:10 -07:00
commit 91d4207fa7
324 files changed
+13509 -6646

No files matched your search

+4 -9
View File
@@ -5,7 +5,7 @@ authors = ["Jonathan Kelley"]
edition = "2021"
description = "CLI tool for developing, testing, and publishing Dioxus apps"
repository = "https://github.com/DioxusLabs/dioxus/"
license = "MIT/Apache-2.0"
license = "MIT OR Apache-2.0"
[dependencies]
# cli core
@@ -57,7 +57,7 @@ flate2 = "1.0.22"
tar = "0.4.38"
zip = "0.6.2"
tower = "0.4.12"
syn = { version = "1.0", features = ["full", "extra-traits"] }
syn = { version = "2.0", features = ["full", "extra-traits"] }
proc-macro2 = { version = "1.0", features = ["span-locations"] }
lazy_static = "1.4.0"
@@ -81,18 +81,13 @@ rsx-rosetta = { workspace = true }
dioxus-rsx = { workspace = true }
dioxus-html = { workspace = true, features = ["hot-reload-context"] }
dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-hot-reload = { workspace = true }
interprocess-docfix = { version = "1.2.2" }
[features]
default = []
plugin = ["mlua"]
# install path dx and dioxus as the same command
# so, they're not really aliases
# eventually dx will defer to the right version of dioxus
[[bin]]
path = "src/main.rs"
name = "dioxus"
[[bin]]
path = "src/main.rs"
name = "dx"
+2 -2
View File
@@ -4,7 +4,7 @@
name = "dioxus-cli"
# default platfrom
# you can also use `dioxus serve/build --platform XXX` to use other platform
# you can also use `dx serve/build --platform XXX` to use other platform
# value: web | desktop
default_platform = "desktop"
@@ -42,4 +42,4 @@ script = []
# use binaryen.wasm-opt for output Wasm file
# binaryen just will trigger in `web` platform
binaryen = { wasm_opt = true }
binaryen = { wasm_opt = true }
+2 -2
View File
@@ -24,7 +24,7 @@ cargo install --path . --debug
## Get Started
Use `dioxus create project-name` to initialize a new Dioxus project. <br>
Use `dx create project-name` to initialize a new Dioxus project. <br>
It will be cloned from the [dioxus-template](https://github.com/DioxusLabs/dioxus-template) repository.
@@ -33,7 +33,7 @@ It will be cloned from the [dioxus-template](https://github.com/DioxusLabs/dioxu
Alternatively, you can specify the template path:
```
dioxus create hello --template gh:dioxuslabs/dioxus-template
dx create hello --template gh:dioxuslabs/dioxus-template
```
## Dioxus Config File
+4 -4
View File
@@ -2,14 +2,14 @@
In this chapter we will introduce all `dioxus-cli` commands.
> You can also use `dioxus --help` to get cli help info.
> You can also use `dx --help` to get cli help info.
```
dioxus
dx
Build, bundle, & ship your Dioxus app
USAGE:
dioxus [OPTIONS] <SUBCOMMAND>
dx [OPTIONS] <SUBCOMMAND>
OPTIONS:
-h, --help Print help information
@@ -23,4 +23,4 @@ SUBCOMMANDS:
help Print this message or the help of the given subcommand(s)
serve Build, watch & serve the Rust WASM app and all of its assets
translate Translate some html file into a Dioxus component
```
```
+17 -8
View File
@@ -1,24 +1,25 @@
# Build
The `dioxus build` command can help you `pack & build` a dioxus project.
The `dx build` command can help you `pack & build` a dioxus project.
```
dioxus-build
dioxus-build
Build the Rust WASM app and all of its assets
USAGE:
dioxus build [OPTIONS]
dx build [OPTIONS]
OPTIONS:
--example <EXAMPLE> [default: ""]
--platform <PLATFORM> [default: "default_platform"]
--release [default: false]
--bin [default: None]
```
You can use this command to build a project:
```
dioxus build --release
dx build --release
```
## Target platform
@@ -27,14 +28,22 @@ Use the `platform` option to choose your target platform:
```
# for desktop project
dioxus build --platform desktop
dx build --platform desktop
```
`platform` currently only supports `desktop` & `web`.
```
# for web project
dioxus build --platform web
dx build --platform web
```
## Specify workspace bin
You can add the `--bin` option to select which crate you want Dioxus to build:
```
dx build --bin app
```
## Build Example
@@ -43,5 +52,5 @@ You can use the `example` option to select a example to build:
```
# build the `test` example
dioxus build --exmaple test
```
dx build --exmaple test
```
+13 -4
View File
@@ -1,18 +1,27 @@
# Clean
`dioxus clean` will clear the build artifacts (the out_dir and the cargo cache)
`dx clean` will clear the build artifacts (the out_dir and the cargo cache)
```
dioxus-clean
dioxus-clean
Clean build artifacts
USAGE:
dioxus clean
dx clean [OPTIONS]
OPTIONS:
--bin [default: None]
```
# Example
```
dioxus clean
dx clean
```
# Specify workspace bin
You can add the `--bin` option to select which crate you want Dioxus to clean artifacts from:
```
dx clean --bin app
```
+19 -10
View File
@@ -1,25 +1,26 @@
# Serve
The `dioxus serve` can start a dev server with hot-reloading
The `dx serve` can start a dev server with hot-reloading
```
dioxus-serve
dioxus-serve
Build, watch & serve the Rust WASM app and all of its assets
USAGE:
dioxus serve [OPTIONS]
dx serve [OPTIONS]
OPTIONS:
--example <EXAMPLE> [default: ""]
--platform <PLATFORM> [default: "default_platform"]
--release [default: false]
--hot-reload [default: false]ß
--hot-reload [default: false]
--bin [default: None]
```
You can use this command to build project and start a dev server:
```
dioxus serve
dx serve
```
## Serve Example
@@ -28,7 +29,15 @@ You can use the `example` option to serve a example:
```
# serve the `test` example
dioxus serve --exmaple test
dx serve --exmaple test
```
## Specify workspace bin
You can add the `--bin` option to select which crate you want Dioxus to build and serve:
```
dx serve --bin app
```
## Open Browser
@@ -36,7 +45,7 @@ dioxus serve --exmaple test
You can add the `--open` option to open system default browser when server startup:
```
dioxus serve --open
dx serve --open
```
## RSX Hot Reloading
@@ -44,7 +53,7 @@ dioxus serve --open
You can add the `--hot-reload` flag to enable [rsx hot reloading](https://dioxuslabs.com/docs/0.3/guide/en/getting_started/hot_reload.html). This will allow you to reload some rsx changes without a full recompile:
```
dioxus serve --open
dx serve --open
```
## Cross Origin Policy
@@ -57,5 +66,5 @@ You can add the `cross-origin-policy` option to change cross-origin header to:
```
```
dioxus serve --corss-origin-policy
```
dx serve --corss-origin-policy
```
+7 -7
View File
@@ -1,13 +1,13 @@
# Translate
`dioxus translate` can translate some `html` file into a Dioxus compoent
`dx translate` can translate some `html` file into a Dioxus compoent
```
dioxus-translate
dioxus-translate
Translate some source file into a Dioxus component
USAGE:
dioxus translate [OPTIONS] [OUTPUT]
dx translate [OPTIONS] [OUTPUT]
ARGS:
<OUTPUT> Output file, defaults to stdout if not present
@@ -22,7 +22,7 @@ OPTIONS:
You can use the `file` option to set path to the `html` file to translate:
```
dioxus transtale --file ./index.html
dx transtale --file ./index.html
```
## Output rsx to a file
@@ -30,7 +30,7 @@ dioxus transtale --file ./index.html
You can pass a file to the traslate command to set the path to write the output of the command to:
```
dioxus translate --file ./index.html component.rsx
dx translate --file ./index.html component.rsx
```
## Output rsx to a file
@@ -38,7 +38,7 @@ dioxus translate --file ./index.html component.rsx
Setting the `component` option will create a compoent from the HTML:
```
dioxus translate --file ./index.html --component
dx translate --file ./index.html --component
```
## Example
@@ -65,4 +65,4 @@ fn component(cx: Scope) -> Element {
}
})
}
```
```
+2 -2
View File
@@ -29,8 +29,8 @@ General application confiration:
# default: web
default_platform = "web"
```
if you change this to `desktop`, the `dioxus build` will default building a desktop app
3. ***out_dir*** - The directory to place the build artifacts from `dioxus build` or `dioxus service` into. This is also where the `assets` directory will be copied to
if you change this to `desktop`, the `dx build` will default building a desktop app
3. ***out_dir*** - The directory to place the build artifacts from `dx build` or `dx service` into. This is also where the `assets` directory will be copied to
```
out_dir = "dist"
```
+4 -4
View File
@@ -4,10 +4,10 @@ Once you have the Dioxus CLI tool installed, you can use it to create dioxus pro
## Initializing a default project
First, run the `dioxus create` command to create a new project ready to be used with Dioxus and the Dioxus CLI:
First, run the `dx create` command to create a new project ready to be used with Dioxus and the Dioxus CLI:
```
dioxus create hello-dioxus
dx create hello-dioxus
```
> It will clone a default template from github template: [DioxusLabs/dioxus-template](https://github.com/DioxusLabs/dioxus-template)
@@ -15,7 +15,7 @@ dioxus create hello-dioxus
>
> You can choose to create your project from a different template by passing the `template` argument:
> ```
> dioxus init hello-dioxus --template=gh:dioxuslabs/dioxus-template
> dx init hello-dioxus --template=gh:dioxuslabs/dioxus-template
> ```
Next, move the current directory into your new project:
@@ -33,7 +33,7 @@ cd hello-dioxus
Finally, create serve your project with the Dioxus CLI:
```
dioxus serve
dx serve
```
By default, the CLI serve your site at: [`http://127.0.0.1:8080/`](http://127.0.0.1:8080/)
+2 -2
View File
@@ -2,7 +2,7 @@
> For Cli 0.2.0 we will add `plugin-develop` support.
Before the 0.2.0 we use `dioxus tool` to use & install some plugin, but we think that is not good for extend cli program, some people want tailwind support, some people want sass support, we can't add all this thing in to the cli source code and we don't have time to maintain a lot of tools that user request, so maybe user make plugin by themself is a good choice.
Before the 0.2.0 we use `dx tool` to use & install some plugin, but we think that is not good for extend cli program, some people want tailwind support, some people want sass support, we can't add all this thing in to the cli source code and we don't have time to maintain a lot of tools that user request, so maybe user make plugin by themself is a good choice.
### Why Lua ?
@@ -76,4 +76,4 @@ end
manager.serve.interval = 1000
return manager
```
```
+1 -1
View File
@@ -4,7 +4,7 @@
name = "{{project-name}}"
# default platfrom
# you can also use `dioxus serve/build --platform XXX` to use other platform
# you can also use `dx serve/build --platform XXX` to use other platform
# value: web | desktop
default_platform = "{{default-platform}}"
+85 -89
View File
@@ -12,7 +12,6 @@ use std::{
io::Read,
panic,
path::PathBuf,
process::Command,
time::Duration,
};
use wasm_bindgen_cli_support::Bindgen;
@@ -161,7 +160,7 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
}
} else {
log::warn!(
"Binaryen tool not found, you can use `dioxus tool add binaryen` to install it."
"Binaryen tool not found, you can use `dx tool add binaryen` to install it."
);
}
}
@@ -200,7 +199,7 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
}
} else {
log::warn!(
"Tailwind tool not found, you can use `dioxus tool add tailwindcss` to install it."
"Tailwind tool not found, you can use `dx tool add tailwindcss` to install it."
);
}
}
@@ -244,128 +243,125 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
})
}
pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<()> {
pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResult> {
log::info!("🚅 Running build [Desktop] command...");
let t_start = std::time::Instant::now();
let ignore_files = build_assets(config)?;
let mut cmd = Command::new("cargo");
cmd.current_dir(&config.crate_dir)
let mut cmd = subprocess::Exec::cmd("cargo")
.cwd(&config.crate_dir)
.arg("build")
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit());
.arg("--message-format=json");
if config.release {
cmd.arg("--release");
cmd = cmd.arg("--release");
}
if config.verbose {
cmd.arg("--verbose");
cmd = cmd.arg("--verbose");
}
if config.custom_profile.is_some() {
let custom_profile = config.custom_profile.as_ref().unwrap();
cmd.arg("--profile");
cmd.arg(custom_profile);
cmd = cmd.arg("--profile").arg(custom_profile);
}
if config.features.is_some() {
let features_str = config.features.as_ref().unwrap().join(" ");
cmd.arg("--features");
cmd.arg(features_str);
cmd = cmd.arg("--features").arg(features_str);
}
match &config.executable {
let cmd = match &config.executable {
crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
crate::ExecutableType::Example(name) => cmd.arg("--example").arg(name),
};
let output = cmd.output()?;
let warning_messages = prettier_build(cmd)?;
if !output.status.success() {
return Err(Error::BuildFailed("Program build failed.".into()));
}
let release_type = match config.release {
true => "release",
false => "debug",
};
if output.status.success() {
let release_type = match config.release {
true => "release",
false => "debug",
};
let file_name: String;
let mut res_path = match &config.executable {
crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
file_name = name.clone();
config.target_dir.join(release_type).join(name)
}
crate::ExecutableType::Example(name) => {
file_name = name.clone();
config
.target_dir
.join(release_type)
.join("examples")
.join(name)
}
};
let target_file = if cfg!(windows) {
res_path.set_extension("exe");
format!("{}.exe", &file_name)
} else {
file_name
};
if !config.out_dir.is_dir() {
create_dir_all(&config.out_dir)?;
let file_name: String;
let mut res_path = match &config.executable {
crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
file_name = name.clone();
config.target_dir.join(release_type).join(name)
}
copy(res_path, &config.out_dir.join(target_file))?;
crate::ExecutableType::Example(name) => {
file_name = name.clone();
config
.target_dir
.join(release_type)
.join("examples")
.join(name)
}
};
// this code will copy all public file to the output dir
if config.asset_dir.is_dir() {
let copy_options = fs_extra::dir::CopyOptions {
overwrite: true,
skip_exist: false,
buffer_size: 64000,
copy_inside: false,
content_only: false,
depth: 0,
};
let target_file = if cfg!(windows) {
res_path.set_extension("exe");
format!("{}.exe", &file_name)
} else {
file_name
};
for entry in std::fs::read_dir(&config.asset_dir)? {
let path = entry?.path();
if path.is_file() {
std::fs::copy(&path, &config.out_dir.join(path.file_name().unwrap()))?;
} else {
match fs_extra::dir::copy(&path, &config.out_dir, &copy_options) {
Ok(_) => {}
Err(e) => {
log::warn!("Error copying dir: {}", e);
}
if !config.out_dir.is_dir() {
create_dir_all(&config.out_dir)?;
}
copy(res_path, &config.out_dir.join(target_file))?;
// this code will copy all public file to the output dir
if config.asset_dir.is_dir() {
let copy_options = fs_extra::dir::CopyOptions {
overwrite: true,
skip_exist: false,
buffer_size: 64000,
copy_inside: false,
content_only: false,
depth: 0,
};
for entry in std::fs::read_dir(&config.asset_dir)? {
let path = entry?.path();
if path.is_file() {
std::fs::copy(&path, &config.out_dir.join(path.file_name().unwrap()))?;
} else {
match fs_extra::dir::copy(&path, &config.out_dir, &copy_options) {
Ok(_) => {}
Err(e) => {
log::warn!("Error copying dir: {}", e);
}
for ignore in &ignore_files {
let ignore = ignore.strip_prefix(&config.asset_dir).unwrap();
let ignore = config.out_dir.join(ignore);
if ignore.is_file() {
std::fs::remove_file(ignore)?;
}
}
for ignore in &ignore_files {
let ignore = ignore.strip_prefix(&config.asset_dir).unwrap();
let ignore = config.out_dir.join(ignore);
if ignore.is_file() {
std::fs::remove_file(ignore)?;
}
}
}
}
log::info!(
"🚩 Build completed: [./{}]",
config
.dioxus_config
.application
.out_dir
.clone()
.unwrap_or_else(|| PathBuf::from("dist"))
.display()
);
}
Ok(())
log::info!(
"🚩 Build completed: [./{}]",
config
.dioxus_config
.application
.out_dir
.clone()
.unwrap_or_else(|| PathBuf::from("dist"))
.display()
);
println!("build desktop done");
Ok(BuildResult {
warnings: warning_messages,
elapsed_time: (t_start - std::time::Instant::now()).as_millis(),
})
}
fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result<Vec<Diagnostic>> {
@@ -78,7 +78,7 @@ impl Autoformat {
///
/// Doesn't do mod-descending, so it will still try to format unreachable files. TODO.
async fn autoformat_project(check: bool) -> Result<()> {
let crate_config = crate::CrateConfig::new()?;
let crate_config = crate::CrateConfig::new(None)?;
let mut files_to_format = vec![];
collect_rs_files(&crate_config.crate_dir, &mut files_to_format);
@@ -109,11 +109,17 @@ async fn autoformat_project(check: bool) -> Result<()> {
})
.await;
if res.is_err() {
eprintln!("error formatting file: {}", _path.display());
match res {
Err(err) => {
eprintln!("error formatting file: {}\n{err}", _path.display());
None
}
Ok(Err(err)) => {
eprintln!("error formatting file: {}\n{err}", _path.display());
None
}
Ok(Ok(res)) => Some(res),
}
res
})
.collect::<FuturesUnordered<_>>()
.collect::<Vec<_>>()
@@ -122,7 +128,7 @@ async fn autoformat_project(check: bool) -> Result<()> {
let files_formatted: usize = counts
.into_iter()
.map(|f| match f {
Ok(Ok(res)) => res,
Some(res) => res,
_ => 0,
})
.sum();
@@ -136,12 +142,16 @@ async fn autoformat_project(check: bool) -> Result<()> {
}
fn collect_rs_files(folder: &Path, files: &mut Vec<PathBuf>) {
let Ok(folder) = folder.read_dir() else { return };
let Ok(folder) = folder.read_dir() else {
return;
};
// load the gitignore
for entry in folder {
let Ok(entry) = entry else { continue; };
let Ok(entry) = entry else {
continue;
};
let path = entry.path();
@@ -1,3 +1,4 @@
use crate::cfg::Platform;
#[cfg(feature = "plugin")]
use crate::plugin::PluginManager;
@@ -12,8 +13,8 @@ pub struct Build {
}
impl Build {
pub fn build(self) -> Result<()> {
let mut crate_config = crate::CrateConfig::new()?;
pub fn build(self, bin: Option<PathBuf>) -> Result<()> {
let mut crate_config = crate::CrateConfig::new(bin)?;
// change the release state.
crate_config.with_release(self.build.release);
@@ -31,27 +32,21 @@ impl Build {
crate_config.set_features(self.build.features.unwrap());
}
let platform = self.build.platform.unwrap_or_else(|| {
crate_config
.dioxus_config
.application
.default_platform
.clone()
});
let platform = self
.build
.platform
.unwrap_or(crate_config.dioxus_config.application.default_platform);
#[cfg(feature = "plugin")]
let _ = PluginManager::on_build_start(&crate_config, &platform);
match platform.as_str() {
"web" => {
match platform {
Platform::Web => {
crate::builder::build(&crate_config, false)?;
}
"desktop" => {
Platform::Desktop => {
crate::builder::build_desktop(&crate_config, false)?;
}
_ => {
return custom_error!("Unsupported platform target.");
}
}
let temp = gen_page(&crate_config.dioxus_config, false);
+17 -4
View File
@@ -1,3 +1,6 @@
use clap::ValueEnum;
use serde::Serialize;
use super::*;
/// Config options for the build system.
@@ -26,8 +29,8 @@ pub struct ConfigOptsBuild {
pub profile: Option<String>,
/// Build platform: support Web & Desktop [default: "default_platform"]
#[clap(long)]
pub platform: Option<String>,
#[clap(long, value_enum)]
pub platform: Option<Platform>,
/// Space separated list of features to activate
#[clap(long)]
@@ -69,8 +72,8 @@ pub struct ConfigOptsServe {
pub profile: Option<String>,
/// Build platform: support Web & Desktop [default: "default_platform"]
#[clap(long)]
pub platform: Option<String>,
#[clap(long, value_enum)]
pub platform: Option<Platform>,
/// Build with hot reloading rsx [default: false]
#[clap(long)]
@@ -88,6 +91,16 @@ pub struct ConfigOptsServe {
pub features: Option<Vec<String>>,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)]
pub enum Platform {
#[clap(name = "web")]
#[serde(rename = "web")]
Web,
#[clap(name = "desktop")]
#[serde(rename = "desktop")]
Desktop,
}
/// Ensure the given value for `--public-url` is formatted correctly.
pub fn parse_public_url(val: &str) -> String {
let prefix = if !val.starts_with('/') { "/" } else { "" };
@@ -6,8 +6,8 @@ use super::*;
pub struct Clean {}
impl Clean {
pub fn clean(self) -> Result<()> {
let crate_config = crate::CrateConfig::new()?;
pub fn clean(self, bin: Option<PathBuf>) -> Result<()> {
let crate_config = crate::CrateConfig::new(bin)?;
let output = Command::new("cargo")
.arg("clean")
@@ -41,19 +41,19 @@ impl Config {
return Ok(());
}
let mut file = File::create(conf_path)?;
let content = String::from(include_str!("../../assets/dioxus.toml"))
let content = String::from(include_str!("../assets/dioxus.toml"))
.replace("{{project-name}}", &name)
.replace("{{default-platform}}", &platform);
file.write_all(content.as_bytes())?;
log::info!("🚩 Init config file completed.");
}
Config::FormatPrint {} => {
println!("{:#?}", crate::CrateConfig::new()?.dioxus_config);
println!("{:#?}", crate::CrateConfig::new(None)?.dioxus_config);
}
Config::CustomHtml {} => {
let html_path = crate_root.join("index.html");
let mut file = File::create(html_path)?;
let content = include_str!("../../assets/index.html");
let content = include_str!("../assets/index.html");
file.write_all(content.as_bytes())?;
log::info!("🚩 Create custom html file done.");
}
File renamed without changes.
+4
View File
@@ -36,6 +36,10 @@ pub struct Cli {
/// Enable verbose logging.
#[clap(short)]
pub v: bool,
/// Specify bin target
#[clap(global = true, long)]
pub bin: Option<String>,
}
#[derive(Parser)]
@@ -31,7 +31,7 @@ impl Plugin {
}
}
Plugin::Add { name: _ } => {
log::info!("You can use `dioxus plugin app-path` to get Installation position");
log::info!("You can use `dx plugin app-path` to get Installation position");
}
}
Ok(())
@@ -1,10 +1,5 @@
use super::*;
use std::{
fs::create_dir_all,
io::Write,
path::PathBuf,
process::{Command, Stdio},
};
use std::{fs::create_dir_all, io::Write, path::PathBuf};
/// Run the WASM project on dev-server
#[derive(Clone, Debug, Parser)]
@@ -15,8 +10,8 @@ pub struct Serve {
}
impl Serve {
pub async fn serve(self) -> Result<()> {
let mut crate_config = crate::CrateConfig::new()?;
pub async fn serve(self, bin: Option<PathBuf>) -> Result<()> {
let mut crate_config = crate::CrateConfig::new(bin)?;
// change the relase state.
crate_config.with_hot_reload(self.serve.hot_reload);
@@ -39,41 +34,24 @@ impl Serve {
// Subdirectories don't work with the server
crate_config.dioxus_config.web.app.base_path = None;
let platform = self.serve.platform.unwrap_or_else(|| {
crate_config
.dioxus_config
.application
.default_platform
.clone()
});
let platform = self
.serve
.platform
.unwrap_or(crate_config.dioxus_config.application.default_platform);
if platform.as_str() == "desktop" {
crate::builder::build_desktop(&crate_config, true)?;
match platform {
cfg::Platform::Web => {
// generate dev-index page
Serve::regen_dev_page(&crate_config)?;
match &crate_config.executable {
crate::ExecutableType::Binary(name)
| crate::ExecutableType::Lib(name)
| crate::ExecutableType::Example(name) => {
let mut file = crate_config.out_dir.join(name);
if cfg!(windows) {
file.set_extension("exe");
}
Command::new(file.to_str().unwrap())
.stdout(Stdio::inherit())
.output()?;
}
// start the develop server
server::web::startup(self.serve.port, crate_config.clone(), self.serve.open)
.await?;
}
cfg::Platform::Desktop => {
server::desktop::startup(crate_config.clone()).await?;
}
return Ok(());
} else if platform != "web" {
return custom_error!("Unsupported platform target.");
}
// generate dev-index page
Serve::regen_dev_page(&crate_config)?;
// start the develop server
server::startup(self.serve.port, crate_config.clone(), self.serve.open).await?;
Ok(())
}
File renamed without changes.
@@ -130,7 +130,7 @@ fn determine_input(file: Option<String>, raw: Option<String>) -> Result<String>
#[test]
fn generates_svgs() {
let st = include_str!("../../../tests/svg.html");
let st = include_str!("../../tests/svg.html");
let out = convert_html_to_formatted_rsx(&html_parser::Dom::parse(st).unwrap(), true);
+52 -22
View File
@@ -1,6 +1,9 @@
use crate::error::Result;
use crate::{cfg::Platform, error::Result};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, path::PathBuf};
use std::{
collections::HashMap,
path::{Path, PathBuf},
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DioxusConfig {
@@ -17,29 +20,49 @@ fn default_plugin() -> toml::Value {
}
impl DioxusConfig {
pub fn load() -> crate::error::Result<Option<DioxusConfig>> {
let Ok(crate_dir) = crate::cargo::crate_root() else { return Ok(None); };
pub fn load(bin: Option<PathBuf>) -> crate::error::Result<Option<DioxusConfig>> {
let crate_dir = crate::cargo::crate_root();
let crate_dir = match crate_dir {
Ok(dir) => {
if let Some(bin) = bin {
dir.join(bin)
} else {
dir
}
}
Err(_) => return Ok(None),
};
let crate_dir = crate_dir.as_path();
// we support either `Dioxus.toml` or `Cargo.toml`
let Some(dioxus_conf_file) = acquire_dioxus_toml(crate_dir) else {
return Ok(None);
};
let dioxus_conf_file = dioxus_conf_file.as_path();
toml::from_str::<DioxusConfig>(&std::fs::read_to_string(dioxus_conf_file)?)
.map_err(|_| crate::Error::Unique("Dioxus.toml parse failed".into()))
.map_err(|err| {
let error_location = dioxus_conf_file
.strip_prefix(crate_dir)
.unwrap_or(dioxus_conf_file)
.display();
crate::Error::Unique(format!("{error_location} {err}"))
})
.map(Some)
}
}
fn acquire_dioxus_toml(dir: PathBuf) -> Option<PathBuf> {
fn acquire_dioxus_toml(dir: &Path) -> Option<PathBuf> {
// prefer uppercase
if dir.join("Dioxus.toml").is_file() {
return Some(dir.join("Dioxus.toml"));
let uppercase_conf = dir.join("Dioxus.toml");
if uppercase_conf.is_file() {
return Some(uppercase_conf);
}
// lowercase is fine too
if dir.join("dioxus.toml").is_file() {
return Some(dir.join("Dioxus.toml"));
let lowercase_conf = dir.join("dioxus.toml");
if lowercase_conf.is_file() {
return Some(lowercase_conf);
}
None
@@ -50,7 +73,7 @@ impl Default for DioxusConfig {
Self {
application: ApplicationConfig {
name: "dioxus".into(),
default_platform: "web".to_string(),
default_platform: Platform::Web,
out_dir: Some(PathBuf::from("dist")),
asset_dir: Some(PathBuf::from("public")),
@@ -92,7 +115,7 @@ impl Default for DioxusConfig {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApplicationConfig {
pub name: String,
pub default_platform: String,
pub default_platform: Platform,
pub out_dir: Option<PathBuf>,
pub asset_dir: Option<PathBuf>,
@@ -176,14 +199,19 @@ pub enum ExecutableType {
}
impl CrateConfig {
pub fn new() -> Result<Self> {
let dioxus_config = DioxusConfig::load()?.unwrap_or_default();
pub fn new(bin: Option<PathBuf>) -> Result<Self> {
let dioxus_config = DioxusConfig::load(bin.clone())?.unwrap_or_default();
let crate_root = crate::cargo::crate_root()?;
let crate_dir = if let Some(package) = &dioxus_config.application.sub_package {
crate::cargo::crate_root()?.join(package)
crate_root.join(package)
} else if let Some(bin) = bin {
crate_root.join(bin)
} else {
crate::cargo::crate_root()?
crate_root
};
let meta = crate::cargo::Metadata::get()?;
let workspace_dir = meta.workspace_root;
let target_dir = meta.target_directory;
@@ -202,8 +230,9 @@ impl CrateConfig {
let manifest = cargo_toml::Manifest::from_path(cargo_def).unwrap();
let output_filename = {
match &manifest.package.as_ref().unwrap().default_run {
let mut output_filename = String::from("dioxus_app");
if let Some(package) = &manifest.package.as_ref() {
output_filename = match &package.default_run {
Some(default_run_target) => default_run_target.to_owned(),
None => manifest
.bin
@@ -216,9 +245,10 @@ impl CrateConfig {
.or(manifest.bin.first())
.or(manifest.lib.as_ref())
.and_then(|prod| prod.name.clone())
.expect("No executable or library found from cargo metadata."),
}
};
.unwrap_or(String::from("dioxus_app")),
};
}
let executable = ExecutableType::Binary(output_filename);
let release = false;
+47 -5
View File
@@ -1,3 +1,5 @@
use std::path::PathBuf;
use anyhow::anyhow;
use clap::Parser;
use dioxus_cli::*;
@@ -7,14 +9,54 @@ use dioxus_cli::plugin::PluginManager;
use Commands::*;
fn get_bin(bin: Option<String>) -> Result<Option<PathBuf>> {
const ERR_MESSAGE: &str = "The `--bin` flag has to be ran in a Cargo workspace.";
if let Some(ref bin) = bin {
let manifest = cargo_toml::Manifest::from_path("./Cargo.toml")
.map_err(|_| Error::CargoError(ERR_MESSAGE.to_string()))?;
if let Some(workspace) = manifest.workspace {
for item in workspace.members.iter() {
let path = PathBuf::from(item);
if !path.exists() {
continue;
}
if !path.is_dir() {
continue;
}
if path.ends_with(bin.clone()) {
return Ok(Some(path));
}
}
} else {
return Err(Error::CargoError(ERR_MESSAGE.to_string()));
}
}
// If the bin exists but we couldn't find it
if bin.is_some() {
return Err(Error::CargoError(
"The specified bin does not exist.".to_string(),
));
}
Ok(None)
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args = Cli::parse();
set_up_logging();
let _dioxus_config = DioxusConfig::load()
.map_err(|e| anyhow!("Failed to load `Dioxus.toml` because: {e}"))?
let bin = get_bin(args.bin)?;
let _dioxus_config = DioxusConfig::load(bin.clone())
.map_err(|e| anyhow!("Failed to load Dioxus config because: {e}"))?
.unwrap_or_else(|| {
log::warn!("You appear to be creating a Dioxus project from scratch; we will use the default config");
DioxusConfig::default()
@@ -30,15 +72,15 @@ async fn main() -> anyhow::Result<()> {
.map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)),
Build(opts) => opts
.build()
.build(bin.clone())
.map_err(|e| anyhow!("🚫 Building project failed: {}", e)),
Clean(opts) => opts
.clean()
.clean(bin.clone())
.map_err(|e| anyhow!("🚫 Cleaning project failed: {}", e)),
Serve(opts) => opts
.serve()
.serve(bin.clone())
.await
.map_err(|e| anyhow!("🚫 Serving project failed: {}", e)),
+248
View File
@@ -0,0 +1,248 @@
use crate::{
server::{
output::{print_console_info, PrettierOptions},
setup_file_watcher, setup_file_watcher_hot_reload,
},
BuildResult, CrateConfig, Result,
};
use dioxus_hot_reload::HotReloadMsg;
use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::*;
use interprocess_docfix::local_socket::LocalSocketListener;
use std::{
process::{Child, Command},
sync::{Arc, Mutex, RwLock},
};
use tokio::sync::broadcast::{self};
#[cfg(feature = "plugin")]
use plugin::PluginManager;
pub async fn startup(config: CrateConfig) -> Result<()> {
// ctrl-c shutdown checker
let _crate_config = config.clone();
let _ = ctrlc::set_handler(move || {
#[cfg(feature = "plugin")]
let _ = PluginManager::on_serve_shutdown(&_crate_config);
std::process::exit(0);
});
match config.hot_reload {
true => serve_hot_reload(config).await?,
false => serve_default(config).await?,
}
Ok(())
}
/// Start the server without hot reload
pub async fn serve_default(config: CrateConfig) -> Result<()> {
let (child, first_build_result) = start_desktop(&config)?;
let currently_running_child: RwLock<Child> = RwLock::new(child);
log::info!("🚀 Starting development server...");
// We got to own watcher so that it exists for the duration of serve
// Otherwise full reload won't work.
let _watcher = setup_file_watcher(
{
let config = config.clone();
move || {
let mut current_child = currently_running_child.write().unwrap();
current_child.kill()?;
let (child, result) = start_desktop(&config)?;
*current_child = child;
Ok(result)
}
},
&config,
None,
)
.await?;
// Print serve info
print_console_info(
&config,
PrettierOptions {
changed: vec![],
warnings: first_build_result.warnings,
elapsed_time: first_build_result.elapsed_time,
},
None,
);
std::future::pending::<()>().await;
Ok(())
}
/// Start the server without hot reload
/// Start dx serve with hot reload
pub async fn serve_hot_reload(config: CrateConfig) -> Result<()> {
let (_, first_build_result) = start_desktop(&config)?;
println!("🚀 Starting development server...");
// Setup hot reload
let FileMapBuildResult { map, errors } =
FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
println!("🚀 Starting development server...");
for err in errors {
log::error!("{}", err);
}
let file_map = Arc::new(Mutex::new(map));
let (hot_reload_tx, mut hot_reload_rx) = broadcast::channel(100);
// States
// The open interprocess sockets
let channels = Arc::new(Mutex::new(Vec::new()));
// Setup file watcher
// We got to own watcher so that it exists for the duration of serve
// Otherwise hot reload won't work.
let _watcher = setup_file_watcher_hot_reload(
&config,
hot_reload_tx,
file_map.clone(),
{
let config = config.clone();
let channels = channels.clone();
move || {
for channel in &mut *channels.lock().unwrap() {
send_msg(HotReloadMsg::Shutdown, channel);
}
Ok(start_desktop(&config)?.1)
}
},
None,
)
.await?;
// Print serve info
print_console_info(
&config,
PrettierOptions {
changed: vec![],
warnings: first_build_result.warnings,
elapsed_time: first_build_result.elapsed_time,
},
None,
);
clear_paths();
match LocalSocketListener::bind("@dioxusin") {
Ok(local_socket_stream) => {
let aborted = Arc::new(Mutex::new(false));
// listen for connections
std::thread::spawn({
let file_map = file_map.clone();
let channels = channels.clone();
let aborted = aborted.clone();
let _ = local_socket_stream.set_nonblocking(true);
move || {
loop {
if let Ok(mut connection) = local_socket_stream.accept() {
// send any templates than have changed before the socket connected
let templates: Vec<_> = {
file_map
.lock()
.unwrap()
.map
.values()
.filter_map(|(_, template_slot)| *template_slot)
.collect()
};
for template in templates {
if !send_msg(
HotReloadMsg::UpdateTemplate(template),
&mut connection,
) {
continue;
}
}
channels.lock().unwrap().push(connection);
println!("Connected to hot reloading 🚀");
}
if *aborted.lock().unwrap() {
break;
}
}
}
});
while let Ok(template) = hot_reload_rx.recv().await {
let channels = &mut *channels.lock().unwrap();
let mut i = 0;
while i < channels.len() {
let channel = &mut channels[i];
if send_msg(HotReloadMsg::UpdateTemplate(template), channel) {
i += 1;
} else {
channels.remove(i);
}
}
}
}
Err(error) => println!("failed to connect to hot reloading\n{error}"),
}
Ok(())
}
fn clear_paths() {
if cfg!(target_os = "macos") {
// On unix, if you force quit the application, it can leave the file socket open
// This will cause the local socket listener to fail to open
// We check if the file socket is already open from an old session and then delete it
let paths = ["./dioxusin", "./@dioxusin"];
for path in paths {
let path = std::path::PathBuf::from(path);
if path.exists() {
let _ = std::fs::remove_file(path);
}
}
}
}
fn send_msg(msg: HotReloadMsg, channel: &mut impl std::io::Write) -> bool {
if let Ok(msg) = serde_json::to_string(&msg) {
if channel.write_all(msg.as_bytes()).is_err() {
return false;
}
if channel.write_all(&[b'\n']).is_err() {
return false;
}
true
} else {
false
}
}
pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> {
// Run the desktop application
let result = crate::builder::build_desktop(config, true)?;
match &config.executable {
crate::ExecutableType::Binary(name)
| crate::ExecutableType::Lib(name)
| crate::ExecutableType::Example(name) => {
let mut file = config.out_dir.join(name);
if cfg!(windows) {
file.set_extension("exe");
}
let child = Command::new(file.to_str().unwrap()).spawn()?;
Ok((child, result))
}
}
}
+17 -490
View File
@@ -1,449 +1,27 @@
use crate::{builder, serve::Serve, BuildResult, CrateConfig, Result};
use axum::{
body::{Full, HttpBody},
extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
http::{
header::{HeaderName, HeaderValue},
Method, Response, StatusCode,
},
response::IntoResponse,
routing::{get, get_service},
Router,
};
use axum_server::tls_rustls::RustlsConfig;
use crate::{BuildResult, CrateConfig, Result};
use cargo_metadata::diagnostic::Diagnostic;
use dioxus_core::Template;
use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::*;
use notify::{RecommendedWatcher, Watcher};
use std::{
net::UdpSocket,
path::PathBuf,
process::Command,
sync::{Arc, Mutex},
};
use tokio::sync::broadcast::{self, Sender};
use tower::ServiceBuilder;
use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody};
use tower_http::{
cors::{Any, CorsLayer},
ServiceBuilderExt,
};
#[cfg(feature = "plugin")]
use plugin::PluginManager;
mod proxy;
mod hot_reload;
use hot_reload::*;
use tokio::sync::broadcast::Sender;
mod output;
use output::*;
pub struct BuildManager {
config: CrateConfig,
reload_tx: broadcast::Sender<()>,
}
impl BuildManager {
fn rebuild(&self) -> Result<BuildResult> {
log::info!("🪁 Rebuild project");
let result = builder::build(&self.config, true)?;
// change the websocket reload state to true;
// the page will auto-reload.
if self
.config
.dioxus_config
.web
.watcher
.reload_html
.unwrap_or(false)
{
let _ = Serve::regen_dev_page(&self.config);
}
let _ = self.reload_tx.send(());
Ok(result)
}
}
struct WsReloadState {
update: broadcast::Sender<()>,
}
pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Result<()> {
// ctrl-c shutdown checker
let _crate_config = config.clone();
let _ = ctrlc::set_handler(move || {
#[cfg(feature = "plugin")]
let _ = PluginManager::on_serve_shutdown(&_crate_config);
std::process::exit(0);
});
let ip = get_ip().unwrap_or(String::from("0.0.0.0"));
match config.hot_reload {
true => serve_hot_reload(ip, port, config, start_browser).await?,
false => serve_default(ip, port, config, start_browser).await?,
}
Ok(())
}
/// Start the server without hot reload
pub async fn serve_default(
ip: String,
port: u16,
config: CrateConfig,
start_browser: bool,
) -> Result<()> {
let first_build_result = crate::builder::build(&config, false)?;
log::info!("🚀 Starting development server...");
// WS Reload Watching
let (reload_tx, _) = broadcast::channel(100);
// We got to own watcher so that it exists for the duration of serve
// Otherwise full reload won't work.
let _watcher = setup_file_watcher(&config, port, ip.clone(), reload_tx.clone()).await?;
let ws_reload_state = Arc::new(WsReloadState {
update: reload_tx.clone(),
});
// HTTPS
// Before console info so it can stop if mkcert isn't installed or fails
let rustls_config = get_rustls(&config).await?;
// Print serve info
print_console_info(
&ip,
port,
&config,
PrettierOptions {
changed: vec![],
warnings: first_build_result.warnings,
elapsed_time: first_build_result.elapsed_time,
},
);
// Router
let router = setup_router(config, ws_reload_state, None).await?;
// Start server
start_server(port, router, start_browser, rustls_config).await?;
Ok(())
}
/// Start dx serve with hot reload
pub async fn serve_hot_reload(
ip: String,
port: u16,
config: CrateConfig,
start_browser: bool,
) -> Result<()> {
let first_build_result = crate::builder::build(&config, false)?;
log::info!("🚀 Starting development server...");
// Setup hot reload
let (reload_tx, _) = broadcast::channel(100);
let FileMapBuildResult { map, errors } =
FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
for err in errors {
log::error!("{}", err);
}
let file_map = Arc::new(Mutex::new(map));
let build_manager = Arc::new(BuildManager {
config: config.clone(),
reload_tx: reload_tx.clone(),
});
let hot_reload_tx = broadcast::channel(100).0;
// States
let hot_reload_state = Arc::new(HotReloadState {
messages: hot_reload_tx.clone(),
build_manager: build_manager.clone(),
file_map: file_map.clone(),
watcher_config: config.clone(),
});
let ws_reload_state = Arc::new(WsReloadState {
update: reload_tx.clone(),
});
// Setup file watcher
// We got to own watcher so that it exists for the duration of serve
// Otherwise hot reload won't work.
let _watcher = setup_file_watcher_hot_reload(
&config,
port,
ip.clone(),
hot_reload_tx,
file_map,
build_manager,
)
.await?;
// HTTPS
// Before console info so it can stop if mkcert isn't installed or fails
let rustls_config = get_rustls(&config).await?;
// Print serve info
print_console_info(
&ip,
port,
&config,
PrettierOptions {
changed: vec![],
warnings: first_build_result.warnings,
elapsed_time: first_build_result.elapsed_time,
},
);
// Router
let router = setup_router(config, ws_reload_state, Some(hot_reload_state)).await?;
// Start server
start_server(port, router, start_browser, rustls_config).await?;
Ok(())
}
const DEFAULT_KEY_PATH: &str = "ssl/key.pem";
const DEFAULT_CERT_PATH: &str = "ssl/cert.pem";
/// Returns an enum of rustls config and a bool if mkcert isn't installed
async fn get_rustls(config: &CrateConfig) -> Result<Option<RustlsConfig>> {
let web_config = &config.dioxus_config.web.https;
if web_config.enabled != Some(true) {
return Ok(None);
}
let (cert_path, key_path) = match web_config.mkcert {
// mkcert, use it
Some(true) => {
// Get paths to store certs, otherwise use ssl/item.pem
let key_path = web_config
.key_path
.clone()
.unwrap_or(DEFAULT_KEY_PATH.to_string());
let cert_path = web_config
.cert_path
.clone()
.unwrap_or(DEFAULT_CERT_PATH.to_string());
// Create ssl directory if using defaults
if key_path == DEFAULT_KEY_PATH && cert_path == DEFAULT_CERT_PATH {
_ = fs::create_dir("ssl");
}
let cmd = Command::new("mkcert")
.args([
"-install",
"-key-file",
&key_path,
"-cert-file",
&cert_path,
"localhost",
"::1",
"127.0.0.1",
])
.spawn();
match cmd {
Err(e) => {
match e.kind() {
io::ErrorKind::NotFound => log::error!("mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions."),
e => log::error!("an error occured while generating mkcert certificates: {}", e.to_string()),
};
return Err("failed to generate mkcert certificates".into());
}
Ok(mut cmd) => {
cmd.wait()?;
}
}
(cert_path, key_path)
}
// not mkcert
Some(false) => {
// get paths to cert & key
if let (Some(key), Some(cert)) =
(web_config.key_path.clone(), web_config.cert_path.clone())
{
(cert, key)
} else {
// missing cert or key
return Err("https is enabled but cert or key path is missing".into());
}
}
// other
_ => return Ok(None),
};
Ok(Some(
RustlsConfig::from_pem_file(cert_path, key_path).await?,
))
}
/// Sets up and returns a router
async fn setup_router(
config: CrateConfig,
ws_reload: Arc<WsReloadState>,
hot_reload: Option<Arc<HotReloadState>>,
) -> Result<Router> {
// Setup cors
let cors = CorsLayer::new()
// allow `GET` and `POST` when accessing the resource
.allow_methods([Method::GET, Method::POST])
// allow requests from any origin
.allow_origin(Any)
.allow_headers(Any);
let (coep, coop) = if config.cross_origin_policy {
(
HeaderValue::from_static("require-corp"),
HeaderValue::from_static("same-origin"),
)
} else {
(
HeaderValue::from_static("unsafe-none"),
HeaderValue::from_static("unsafe-none"),
)
};
// Create file service
let file_service_config = config.clone();
let file_service = ServiceBuilder::new()
.override_response_header(
HeaderName::from_static("cross-origin-embedder-policy"),
coep,
)
.override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
.and_then(
move |response: Response<ServeFileSystemResponseBody>| async move {
let response = if file_service_config
.dioxus_config
.web
.watcher
.index_on_404
.unwrap_or(false)
&& response.status() == StatusCode::NOT_FOUND
{
let body = Full::from(
// TODO: Cache/memoize this.
std::fs::read_to_string(
file_service_config
.crate_dir
.join(file_service_config.out_dir)
.join("index.html"),
)
.ok()
.unwrap(),
)
.map_err(|err| match err {})
.boxed();
Response::builder()
.status(StatusCode::OK)
.body(body)
.unwrap()
} else {
response.map(|body| body.boxed())
};
Ok(response)
},
)
.service(ServeDir::new(config.crate_dir.join(&config.out_dir)));
// Setup websocket
let mut router = Router::new().route("/_dioxus/ws", get(ws_handler));
// Setup proxy
for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() {
router = proxy::add_proxy(router, &proxy_config)?;
}
// Route file service
router = router.fallback(get_service(file_service).handle_error(
|error: std::io::Error| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", error),
)
},
));
// Setup routes
router = router
.route("/_dioxus/hot_reload", get(hot_reload_handler))
.layer(cors)
.layer(Extension(ws_reload));
if let Some(hot_reload) = hot_reload {
router = router.layer(Extension(hot_reload))
}
Ok(router)
}
/// Starts dx serve with no hot reload
async fn start_server(
port: u16,
router: Router,
start_browser: bool,
rustls: Option<RustlsConfig>,
) -> Result<()> {
// If plugins, call on_serve_start event
#[cfg(feature = "plugin")]
PluginManager::on_serve_start(&config)?;
// Parse address
let addr = format!("0.0.0.0:{}", port).parse().unwrap();
// Open the browser
if start_browser {
match rustls {
Some(_) => _ = open::that(format!("https://{}", addr)),
None => _ = open::that(format!("http://{}", addr)),
}
}
// Start the server with or without rustls
match rustls {
Some(rustls) => {
axum_server::bind_rustls(addr, rustls)
.serve(router.into_make_service())
.await?
}
None => {
axum::Server::bind(&addr)
.serve(router.into_make_service())
.await?
}
}
Ok(())
}
pub mod desktop;
pub mod web;
/// Sets up a file watcher
async fn setup_file_watcher(
async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
build_with: F,
config: &CrateConfig,
port: u16,
watcher_ip: String,
reload_tx: Sender<()>,
web_info: Option<WebServerInfo>,
) -> Result<RecommendedWatcher> {
let build_manager = BuildManager {
config: config.clone(),
reload_tx,
};
let mut last_update_time = chrono::Local::now().timestamp();
// file watcher: check file change
@@ -460,20 +38,19 @@ async fn setup_file_watcher(
let config = watcher_config.clone();
if let Ok(e) = info {
if chrono::Local::now().timestamp() > last_update_time {
match build_manager.rebuild() {
match build_with() {
Ok(res) => {
last_update_time = chrono::Local::now().timestamp();
#[allow(clippy::redundant_clone)]
print_console_info(
&watcher_ip,
port,
&config,
PrettierOptions {
changed: e.paths.clone(),
warnings: res.warnings,
elapsed_time: res.elapsed_time,
},
web_info.clone(),
);
#[cfg(feature = "plugin")]
@@ -502,13 +79,12 @@ async fn setup_file_watcher(
// Todo: reduce duplication and merge with setup_file_watcher()
/// Sets up a file watcher with hot reload
async fn setup_file_watcher_hot_reload(
async fn setup_file_watcher_hot_reload<F: Fn() -> Result<BuildResult> + Send + 'static>(
config: &CrateConfig,
port: u16,
watcher_ip: String,
hot_reload_tx: Sender<Template<'static>>,
file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
build_manager: Arc<BuildManager>,
build_with: F,
web_info: Option<WebServerInfo>,
) -> Result<RecommendedWatcher> {
// file watcher: check file change
let allow_watch_path = config
@@ -533,17 +109,16 @@ async fn setup_file_watcher_hot_reload(
for path in evt.paths.clone() {
// if this is not a rust file, rebuild the whole project
if path.extension().and_then(|p| p.to_str()) != Some("rs") {
match build_manager.rebuild() {
match build_with() {
Ok(res) => {
print_console_info(
&watcher_ip,
port,
&config,
PrettierOptions {
changed: evt.paths,
warnings: res.warnings,
elapsed_time: res.elapsed_time,
},
web_info.clone(),
);
}
Err(err) => {
@@ -560,17 +135,16 @@ async fn setup_file_watcher_hot_reload(
messages.extend(msgs);
}
Ok(UpdateResult::NeedsRebuild) => {
match build_manager.rebuild() {
match build_with() {
Ok(res) => {
print_console_info(
&watcher_ip,
port,
&config,
PrettierOptions {
changed: evt.paths,
warnings: res.warnings,
elapsed_time: res.elapsed_time,
},
web_info.clone(),
);
}
Err(err) => {
@@ -606,50 +180,3 @@ async fn setup_file_watcher_hot_reload(
Ok(watcher)
}
/// Get the network ip
fn get_ip() -> Option<String> {
let socket = match UdpSocket::bind("0.0.0.0:0") {
Ok(s) => s,
Err(_) => return None,
};
match socket.connect("8.8.8.8:80") {
Ok(()) => (),
Err(_) => return None,
};
match socket.local_addr() {
Ok(addr) => Some(addr.ip().to_string()),
Err(_) => None,
}
}
/// Handle websockets
async fn ws_handler(
ws: WebSocketUpgrade,
_: Option<TypedHeader<headers::UserAgent>>,
Extension(state): Extension<Arc<WsReloadState>>,
) -> impl IntoResponse {
ws.on_upgrade(|mut socket| async move {
let mut rx = state.update.subscribe();
let reload_watcher = tokio::spawn(async move {
loop {
rx.recv().await.unwrap();
// ignore the error
if socket
.send(Message::Text(String::from("reload")))
.await
.is_err()
{
break;
}
// flush the errors after recompling
rx = rx.resubscribe();
}
});
reload_watcher.await.unwrap();
})
}
+33 -21
View File
@@ -11,7 +11,17 @@ pub struct PrettierOptions {
pub elapsed_time: u128,
}
pub fn print_console_info(ip: &String, port: u16, config: &CrateConfig, options: PrettierOptions) {
#[derive(Debug, Clone)]
pub struct WebServerInfo {
pub ip: String,
pub port: u16,
}
pub fn print_console_info(
config: &CrateConfig,
options: PrettierOptions,
web_info: Option<WebServerInfo>,
) {
if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") {
"cls"
} else {
@@ -70,26 +80,28 @@ pub fn print_console_info(ip: &String, port: u16, config: &CrateConfig, options:
);
}
if config.dioxus_config.web.https.enabled == Some(true) {
println!(
"\t> Local : {}",
format!("https://localhost:{}/", port).blue()
);
println!(
"\t> Network : {}",
format!("https://{}:{}/", ip, port).blue()
);
println!("\t> HTTPS : {}", "Enabled".to_string().green());
} else {
println!(
"\t> Local : {}",
format!("http://localhost:{}/", port).blue()
);
println!(
"\t> Network : {}",
format!("http://{}:{}/", ip, port).blue()
);
println!("\t> HTTPS : {}", "Disabled".to_string().red());
if let Some(WebServerInfo { ip, port }) = web_info {
if config.dioxus_config.web.https.enabled == Some(true) {
println!(
"\t> Local : {}",
format!("https://localhost:{}/", port).blue()
);
println!(
"\t> Network : {}",
format!("https://{}:{}/", ip, port).blue()
);
println!("\t> HTTPS : {}", "Enabled".to_string().green());
} else {
println!(
"\t> Local : {}",
format!("http://localhost:{}/", port).blue()
);
println!(
"\t> Network : {}",
format!("http://{}:{}/", ip, port).blue()
);
println!("\t> HTTPS : {}", "Disabled".to_string().red());
}
}
println!();
println!("\t> Profile : {}", profile.green());
@@ -10,12 +10,10 @@ use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::FileMap;
use tokio::sync::broadcast;
use super::BuildManager;
use crate::CrateConfig;
pub struct HotReloadState {
pub messages: broadcast::Sender<Template<'static>>,
pub build_manager: Arc<BuildManager>,
pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
pub watcher_config: CrateConfig,
}
+490
View File
@@ -0,0 +1,490 @@
use crate::{
builder,
serve::Serve,
server::{
output::{print_console_info, PrettierOptions, WebServerInfo},
setup_file_watcher, setup_file_watcher_hot_reload,
},
BuildResult, CrateConfig, Result,
};
use axum::{
body::{Full, HttpBody},
extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
http::{
header::{HeaderName, HeaderValue},
Method, Response, StatusCode,
},
response::IntoResponse,
routing::{get, get_service},
Router,
};
use axum_server::tls_rustls::RustlsConfig;
use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::*;
use std::{
net::UdpSocket,
process::Command,
sync::{Arc, Mutex},
};
use tokio::sync::broadcast::{self, Sender};
use tower::ServiceBuilder;
use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody};
use tower_http::{
cors::{Any, CorsLayer},
ServiceBuilderExt,
};
#[cfg(feature = "plugin")]
use plugin::PluginManager;
mod proxy;
mod hot_reload;
use hot_reload::*;
struct WsReloadState {
update: broadcast::Sender<()>,
}
pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Result<()> {
// ctrl-c shutdown checker
let _crate_config = config.clone();
let _ = ctrlc::set_handler(move || {
#[cfg(feature = "plugin")]
let _ = PluginManager::on_serve_shutdown(&_crate_config);
std::process::exit(0);
});
let ip = get_ip().unwrap_or(String::from("0.0.0.0"));
match config.hot_reload {
true => serve_hot_reload(ip, port, config, start_browser).await?,
false => serve_default(ip, port, config, start_browser).await?,
}
Ok(())
}
/// Start the server without hot reload
pub async fn serve_default(
ip: String,
port: u16,
config: CrateConfig,
start_browser: bool,
) -> Result<()> {
let first_build_result = crate::builder::build(&config, false)?;
log::info!("🚀 Starting development server...");
// WS Reload Watching
let (reload_tx, _) = broadcast::channel(100);
// We got to own watcher so that it exists for the duration of serve
// Otherwise full reload won't work.
let _watcher = setup_file_watcher(
{
let config = config.clone();
let reload_tx = reload_tx.clone();
move || build(&config, &reload_tx)
},
&config,
Some(WebServerInfo {
ip: ip.clone(),
port,
}),
)
.await?;
let ws_reload_state = Arc::new(WsReloadState {
update: reload_tx.clone(),
});
// HTTPS
// Before console info so it can stop if mkcert isn't installed or fails
let rustls_config = get_rustls(&config).await?;
// Print serve info
print_console_info(
&config,
PrettierOptions {
changed: vec![],
warnings: first_build_result.warnings,
elapsed_time: first_build_result.elapsed_time,
},
Some(crate::server::output::WebServerInfo {
ip: ip.clone(),
port,
}),
);
// Router
let router = setup_router(config, ws_reload_state, None).await?;
// Start server
start_server(port, router, start_browser, rustls_config).await?;
Ok(())
}
/// Start dx serve with hot reload
pub async fn serve_hot_reload(
ip: String,
port: u16,
config: CrateConfig,
start_browser: bool,
) -> Result<()> {
let first_build_result = crate::builder::build(&config, false)?;
log::info!("🚀 Starting development server...");
// Setup hot reload
let (reload_tx, _) = broadcast::channel(100);
let FileMapBuildResult { map, errors } =
FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
for err in errors {
log::error!("{}", err);
}
let file_map = Arc::new(Mutex::new(map));
let hot_reload_tx = broadcast::channel(100).0;
// States
let hot_reload_state = Arc::new(HotReloadState {
messages: hot_reload_tx.clone(),
file_map: file_map.clone(),
watcher_config: config.clone(),
});
let ws_reload_state = Arc::new(WsReloadState {
update: reload_tx.clone(),
});
// Setup file watcher
// We got to own watcher so that it exists for the duration of serve
// Otherwise hot reload won't work.
let _watcher = setup_file_watcher_hot_reload(
&config,
hot_reload_tx,
file_map,
{
let config = config.clone();
let reload_tx = reload_tx.clone();
move || build(&config, &reload_tx)
},
Some(WebServerInfo {
ip: ip.clone(),
port,
}),
)
.await?;
// HTTPS
// Before console info so it can stop if mkcert isn't installed or fails
let rustls_config = get_rustls(&config).await?;
// Print serve info
print_console_info(
&config,
PrettierOptions {
changed: vec![],
warnings: first_build_result.warnings,
elapsed_time: first_build_result.elapsed_time,
},
Some(WebServerInfo {
ip: ip.clone(),
port,
}),
);
// Router
let router = setup_router(config, ws_reload_state, Some(hot_reload_state)).await?;
// Start server
start_server(port, router, start_browser, rustls_config).await?;
Ok(())
}
const DEFAULT_KEY_PATH: &str = "ssl/key.pem";
const DEFAULT_CERT_PATH: &str = "ssl/cert.pem";
/// Returns an enum of rustls config and a bool if mkcert isn't installed
async fn get_rustls(config: &CrateConfig) -> Result<Option<RustlsConfig>> {
let web_config = &config.dioxus_config.web.https;
if web_config.enabled != Some(true) {
return Ok(None);
}
let (cert_path, key_path) = match web_config.mkcert {
// mkcert, use it
Some(true) => {
// Get paths to store certs, otherwise use ssl/item.pem
let key_path = web_config
.key_path
.clone()
.unwrap_or(DEFAULT_KEY_PATH.to_string());
let cert_path = web_config
.cert_path
.clone()
.unwrap_or(DEFAULT_CERT_PATH.to_string());
// Create ssl directory if using defaults
if key_path == DEFAULT_KEY_PATH && cert_path == DEFAULT_CERT_PATH {
_ = fs::create_dir("ssl");
}
let cmd = Command::new("mkcert")
.args([
"-install",
"-key-file",
&key_path,
"-cert-file",
&cert_path,
"localhost",
"::1",
"127.0.0.1",
])
.spawn();
match cmd {
Err(e) => {
match e.kind() {
io::ErrorKind::NotFound => log::error!("mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions."),
e => log::error!("an error occured while generating mkcert certificates: {}", e.to_string()),
};
return Err("failed to generate mkcert certificates".into());
}
Ok(mut cmd) => {
cmd.wait()?;
}
}
(cert_path, key_path)
}
// not mkcert
Some(false) => {
// get paths to cert & key
if let (Some(key), Some(cert)) =
(web_config.key_path.clone(), web_config.cert_path.clone())
{
(cert, key)
} else {
// missing cert or key
return Err("https is enabled but cert or key path is missing".into());
}
}
// other
_ => return Ok(None),
};
Ok(Some(
RustlsConfig::from_pem_file(cert_path, key_path).await?,
))
}
/// Sets up and returns a router
async fn setup_router(
config: CrateConfig,
ws_reload: Arc<WsReloadState>,
hot_reload: Option<Arc<HotReloadState>>,
) -> Result<Router> {
// Setup cors
let cors = CorsLayer::new()
// allow `GET` and `POST` when accessing the resource
.allow_methods([Method::GET, Method::POST])
// allow requests from any origin
.allow_origin(Any)
.allow_headers(Any);
let (coep, coop) = if config.cross_origin_policy {
(
HeaderValue::from_static("require-corp"),
HeaderValue::from_static("same-origin"),
)
} else {
(
HeaderValue::from_static("unsafe-none"),
HeaderValue::from_static("unsafe-none"),
)
};
// Create file service
let file_service_config = config.clone();
let file_service = ServiceBuilder::new()
.override_response_header(
HeaderName::from_static("cross-origin-embedder-policy"),
coep,
)
.override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
.and_then(
move |response: Response<ServeFileSystemResponseBody>| async move {
let response = if file_service_config
.dioxus_config
.web
.watcher
.index_on_404
.unwrap_or(false)
&& response.status() == StatusCode::NOT_FOUND
{
let body = Full::from(
// TODO: Cache/memoize this.
std::fs::read_to_string(
file_service_config
.crate_dir
.join(file_service_config.out_dir)
.join("index.html"),
)
.ok()
.unwrap(),
)
.map_err(|err| match err {})
.boxed();
Response::builder()
.status(StatusCode::OK)
.body(body)
.unwrap()
} else {
response.map(|body| body.boxed())
};
Ok(response)
},
)
.service(ServeDir::new(config.crate_dir.join(&config.out_dir)));
// Setup websocket
let mut router = Router::new().route("/_dioxus/ws", get(ws_handler));
// Setup proxy
for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() {
router = proxy::add_proxy(router, &proxy_config)?;
}
// Route file service
router = router.fallback(get_service(file_service).handle_error(
|error: std::io::Error| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", error),
)
},
));
// Setup routes
router = router
.route("/_dioxus/hot_reload", get(hot_reload_handler))
.layer(cors)
.layer(Extension(ws_reload));
if let Some(hot_reload) = hot_reload {
router = router.layer(Extension(hot_reload))
}
Ok(router)
}
/// Starts dx serve with no hot reload
async fn start_server(
port: u16,
router: Router,
start_browser: bool,
rustls: Option<RustlsConfig>,
) -> Result<()> {
// If plugins, call on_serve_start event
#[cfg(feature = "plugin")]
PluginManager::on_serve_start(&config)?;
// Parse address
let addr = format!("0.0.0.0:{}", port).parse().unwrap();
// Open the browser
if start_browser {
match rustls {
Some(_) => _ = open::that(format!("https://{}", addr)),
None => _ = open::that(format!("http://{}", addr)),
}
}
// Start the server with or without rustls
match rustls {
Some(rustls) => {
axum_server::bind_rustls(addr, rustls)
.serve(router.into_make_service())
.await?
}
None => {
axum::Server::bind(&addr)
.serve(router.into_make_service())
.await?
}
}
Ok(())
}
/// Get the network ip
fn get_ip() -> Option<String> {
let socket = match UdpSocket::bind("0.0.0.0:0") {
Ok(s) => s,
Err(_) => return None,
};
match socket.connect("8.8.8.8:80") {
Ok(()) => (),
Err(_) => return None,
};
match socket.local_addr() {
Ok(addr) => Some(addr.ip().to_string()),
Err(_) => None,
}
}
/// Handle websockets
async fn ws_handler(
ws: WebSocketUpgrade,
_: Option<TypedHeader<headers::UserAgent>>,
Extension(state): Extension<Arc<WsReloadState>>,
) -> impl IntoResponse {
ws.on_upgrade(|mut socket| async move {
let mut rx = state.update.subscribe();
let reload_watcher = tokio::spawn(async move {
loop {
rx.recv().await.unwrap();
// ignore the error
if socket
.send(Message::Text(String::from("reload")))
.await
.is_err()
{
break;
}
// flush the errors after recompling
rx = rx.resubscribe();
}
});
reload_watcher.await.unwrap();
})
}
fn build(config: &CrateConfig, reload_tx: &Sender<()>) -> Result<BuildResult> {
let result = builder::build(config, true)?;
// change the websocket reload state to true;
// the page will auto-reload.
if config
.dioxus_config
.web
.watcher
.reload_html
.unwrap_or(false)
{
let _ = Serve::regen_dev_page(config);
}
let _ = reload_tx.send(());
Ok(result)
}
File renamed without changes.