more clippy fixes

This commit is contained in:
Evan Almloff 2023-07-20 09:06:12 -07:00
commit beb56b93a0
48 changed files with 1327 additions and 1415 deletions

View file

@ -3,6 +3,10 @@ name: docs stable
on:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build-deploy:
runs-on: ubuntu-latest

View file

@ -8,6 +8,10 @@ on:
branches:
- master
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build-deploy:
runs-on: ubuntu-latest

View file

@ -1,38 +0,0 @@
name: macOS tests
on:
push:
branches:
- master
paths:
- packages/**
- examples/**
- src/**
- .github/**
- lib.rs
- Cargo.toml
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches:
- master
paths:
- packages/**
- examples/**
- src/**
- .github/**
- lib.rs
- Cargo.toml
jobs:
test:
if: github.event.pull_request.draft == false
name: Test Suite
runs-on: macos-latest
steps:
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: actions/checkout@v3
- run: |
cargo test --all --tests
cargo test --package fermi --release

View file

@ -27,6 +27,10 @@ on:
- lib.rs
- Cargo.toml
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
check:
if: github.event.pull_request.draft == false
@ -79,6 +83,69 @@ jobs:
- uses: actions/checkout@v3
- run: cargo clippy --workspace --examples --tests -- -D warnings
matrix_test:
runs-on: ${{ matrix.platform.os }}
strategy:
matrix:
platform:
- {
target: x86_64-pc-windows-msvc,
os: windows-latest,
toolchain: '1.70.0',
cross: false,
command: 'test',
args: '--all --tests'
}
- {
target: x86_64-apple-darwin,
os: macos-latest,
toolchain: '1.70.0',
cross: false,
command: 'test',
args: '--all --tests'
}
- {
target: aarch64-apple-ios,
os: macos-latest,
toolchain: '1.70.0',
cross: false,
command: 'build',
args: '--package dioxus-mobile'
}
- {
target: aarch64-linux-android,
os: ubuntu-latest,
toolchain: '1.70.0',
cross: true,
command: 'build',
args: '--package dioxus-mobile'
}
steps:
- uses: actions/checkout@v2
- name: install stable
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.platform.toolchain }}
target: ${{ matrix.platform.target }}
override: true
default: true
- uses: Swatinem/rust-cache@v2
with:
workspaces: core -> ../target
save-if: ${{ matrix.features.key == 'all' }}
- name: test
uses: actions-rs/cargo@v1
with:
use-cross: ${{ matrix.platform.cross }}
command: ${{ matrix.platform.command }}
args: --target ${{ matrix.platform.target }} ${{ matrix.platform.args }}
# Coverage is disabled until we can fix it
# coverage:
# name: Coverage
@ -99,3 +166,4 @@ jobs:
# uses: codecov/codecov-action@v2
# with:
# fail_ci_if_error: false

View file

@ -6,6 +6,13 @@ on:
branches:
- 'auto'
- 'try'
paths:
- packages/**
- examples/**
- src/**
- .github/**
- lib.rs
- Cargo.toml
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches:
@ -31,7 +38,9 @@ env:
# - tokio-stream/Cargo.toml
# rust_min: 1.49.0
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
test:
@ -72,7 +81,7 @@ jobs:
# #[tokio::main] that calls epoll_create1 that Miri does not support.
# run: cargo miri test --features full --lib --no-fail-fast
run: |
cargo miri test --package dioxus-core --test miri_stress -- --exact --nocapture
cargo miri test --package dioxus-core -- --exact --nocapture
cargo miri test --package dioxus-native-core --test miri_native -- --exact --nocapture
# working-directory: tokio

View file

@ -4,22 +4,25 @@ on:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
defaults:
run:
working-directory: ./playwright-tests
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
test:
if: github.event.pull_request.draft == false
timeout-minutes: 60
runs-on: ubuntu-20.04
steps:
# Do our best to cache the toolchain and node install steps
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npm install -D @playwright/test
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
@ -29,11 +32,18 @@ jobs:
- uses: Swatinem/rust-cache@v2
- name: Install WASM toolchain
run: rustup target add wasm32-unknown-unknown
- name: Install Dioxus CLI
uses: actions-rs/cargo@v1
with:
command: install
args: --path packages/cli
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npm install -D @playwright/test
- name: Install Playwright Browsers
run: npx playwright install --with-deps
# Cache the CLI by using cargo run internally
# - name: Install Dioxus CLI
# uses: actions-rs/cargo@v1
# with:
# command: install
# args: --path packages/cli
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v3

View file

@ -1,89 +0,0 @@
name: windows
on:
push:
branches:
- master
paths:
- packages/**
- examples/**
- src/**
- .github/**
- lib.rs
- Cargo.toml
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches:
- master
paths:
- packages/**
- examples/**
- src/**
- .github/**
- lib.rs
- Cargo.toml
jobs:
test:
if: github.event.pull_request.draft == false
runs-on: windows-latest
name: (${{ matrix.target }}, ${{ matrix.cfg_release_channel }})
env:
CFG_RELEASE_CHANNEL: ${{ matrix.cfg_release_channel }}
strategy:
# https://help.github.com/en/actions/getting-started-with-github-actions/about-github-actions#usage-limits
# There's a limit of 60 concurrent jobs across all repos in the rust-lang organization.
# In order to prevent overusing too much of that 60 limit, we throttle the
# number of rustfmt jobs that will run concurrently.
# max-parallel:
# fail-fast: false
matrix:
target: [x86_64-pc-windows-gnu, x86_64-pc-windows-msvc]
cfg_release_channel: [stable]
steps:
# The Windows runners have autocrlf enabled by default
# which causes failures for some of rustfmt's line-ending sensitive tests
- name: disable git eol translation
run: git config --global core.autocrlf false
# Run build
- name: Install Rustup using win.rustup.rs
run: |
# Disable the download progress bar which can cause perf issues
$ProgressPreference = "SilentlyContinue"
Invoke-WebRequest https://win.rustup.rs/ -OutFile rustup-init.exe
.\rustup-init.exe -y --default-host=x86_64-pc-windows-msvc --default-toolchain=none
del rustup-init.exe
rustup target add ${{ matrix.target }}
shell: powershell
- name: Add mingw64 to path for x86_64-gnu
run: echo "C:\msys64\mingw64\bin" >> $GITHUB_PATH
if: matrix.target == 'x86_64-pc-windows-gnu' && matrix.channel == 'nightly'
shell: bash
# - name: checkout
# uses: actions/checkout@v3
# with:
# path: C:/dioxus.git
# fetch-depth: 1
# we need to use the C drive as the working directory
- name: Checkout
run: |
mkdir C:/dioxus.git
git clone https://github.com/dioxuslabs/dioxus.git C:/dioxus.git --depth 1
- name: test
working-directory: C:/dioxus.git
run: |
rustc -Vv
cargo -V
set RUST_BACKTRACE=1
cargo build --all --tests --examples
cargo test --all --tests
cargo test --package fermi --release
shell: cmd

View file

@ -14,7 +14,7 @@ keywords = ["dom", "ui", "gui", "react"]
dioxus-rsx = { workspace = true }
proc-macro2 = { version = "1.0.6", features = ["span-locations"] }
quote = "1.0"
syn = { version = "2.0", features = ["full", "extra-traits"] }
syn = { version = "2.0", features = ["full", "extra-traits", "visit"] }
serde = { version = "1.0.136", features = ["derive"] }
prettyplease = { package = "prettier-please", version = "0.2", features = [
"verbatim",

View file

@ -3,180 +3,23 @@
//! Returns all macros that match a pattern. You can use this information to autoformat them later
use proc_macro2::LineColumn;
use syn::{Block, Expr, File, Item, Macro, Stmt};
use syn::{visit::Visit, File, Macro};
type CollectedMacro<'a> = &'a Macro;
pub fn collect_from_file<'a>(file: &'a File, macros: &mut Vec<CollectedMacro<'a>>) {
for item in file.items.iter() {
collect_from_item(item, macros);
}
MacroCollector::visit_file(&mut MacroCollector { macros }, file);
}
pub fn collect_from_item<'a>(item: &'a Item, macros: &mut Vec<CollectedMacro<'a>>) {
match item {
Item::Fn(f) => collect_from_block(&f.block, macros),
// Ignore macros if they're not rsx or render
Item::Macro(macro_) => {
if macro_.mac.path.segments[0].ident == "rsx"
|| macro_.mac.path.segments[0].ident == "render"
{
macros.push(&macro_.mac);
}
struct MacroCollector<'a, 'b> {
macros: &'a mut Vec<CollectedMacro<'b>>,
}
// Currently disabled since we're not focused on autoformatting these
Item::Impl(_imp) => {}
Item::Trait(_) => {}
// Global-ish things
Item::Static(f) => collect_from_expr(&f.expr, macros),
Item::Const(f) => collect_from_expr(&f.expr, macros),
Item::Mod(s) => {
if let Some((_, block)) = &s.content {
for item in block {
collect_from_item(item, macros);
impl<'a, 'b> Visit<'b> for MacroCollector<'a, 'b> {
fn visit_macro(&mut self, i: &'b Macro) {
self.macros.push(i);
}
}
}
// None of these we can really do anything with at the item level
Item::Enum(_)
| Item::ExternCrate(_)
| Item::ForeignMod(_)
| Item::TraitAlias(_)
| Item::Type(_)
| Item::Struct(_)
| Item::Union(_)
| Item::Use(_)
| Item::Verbatim(_) => {}
_ => {}
}
}
pub fn collect_from_block<'a>(block: &'a Block, macros: &mut Vec<CollectedMacro<'a>>) {
for stmt in &block.stmts {
match stmt {
Stmt::Item(item) => collect_from_item(item, macros),
Stmt::Local(local) => {
if let Some(init) = &local.init {
collect_from_expr(&init.expr, macros);
}
}
Stmt::Expr(expr, _) => collect_from_expr(expr, macros),
Stmt::Macro(mac) => {
if mac.mac.path.segments[0].ident == "rsx"
|| mac.mac.path.segments[0].ident == "render"
{
macros.push(&mac.mac);
}
}
}
}
}
pub fn collect_from_expr<'a>(expr: &'a Expr, macros: &mut Vec<CollectedMacro<'a>>) {
// collect an expr from the exprs, descending into blocks
match expr {
Expr::Macro(macro_) => {
if macro_.mac.path.segments[0].ident == "rsx"
|| macro_.mac.path.segments[0].ident == "render"
{
macros.push(&macro_.mac);
}
}
Expr::MethodCall(e) => {
collect_from_expr(&e.receiver, macros);
for expr in e.args.iter() {
collect_from_expr(expr, macros);
}
}
Expr::Assign(exp) => {
collect_from_expr(&exp.left, macros);
collect_from_expr(&exp.right, macros);
}
Expr::Async(b) => collect_from_block(&b.block, macros),
Expr::Block(b) => collect_from_block(&b.block, macros),
Expr::Closure(c) => collect_from_expr(&c.body, macros),
Expr::Let(l) => collect_from_expr(&l.expr, macros),
Expr::Unsafe(u) => collect_from_block(&u.block, macros),
Expr::Loop(l) => collect_from_block(&l.body, macros),
Expr::Call(c) => {
collect_from_expr(&c.func, macros);
for expr in c.args.iter() {
collect_from_expr(expr, macros);
}
}
Expr::ForLoop(b) => {
collect_from_expr(&b.expr, macros);
collect_from_block(&b.body, macros);
}
Expr::If(f) => {
collect_from_expr(&f.cond, macros);
collect_from_block(&f.then_branch, macros);
if let Some((_, else_branch)) = &f.else_branch {
collect_from_expr(else_branch, macros);
}
}
Expr::Yield(y) => {
if let Some(expr) = &y.expr {
collect_from_expr(expr, macros);
}
}
Expr::Return(r) => {
if let Some(expr) = &r.expr {
collect_from_expr(expr, macros);
}
}
Expr::Match(l) => {
collect_from_expr(&l.expr, macros);
for arm in l.arms.iter() {
if let Some((_, expr)) = &arm.guard {
collect_from_expr(expr, macros);
}
collect_from_expr(&arm.body, macros);
}
}
Expr::While(w) => {
collect_from_expr(&w.cond, macros);
collect_from_block(&w.body, macros);
}
// don't both formatting these for now
Expr::Array(_)
| Expr::Await(_)
| Expr::Binary(_)
| Expr::Break(_)
| Expr::Cast(_)
| Expr::Continue(_)
| Expr::Field(_)
| Expr::Group(_)
| Expr::Index(_)
| Expr::Lit(_)
| Expr::Paren(_)
| Expr::Path(_)
| Expr::Range(_)
| Expr::Reference(_)
| Expr::Repeat(_)
| Expr::Struct(_)
| Expr::Try(_)
| Expr::TryBlock(_)
| Expr::Tuple(_)
| Expr::Unary(_)
| Expr::Verbatim(_) => {}
_ => {}
};
}
pub fn byte_offset(input: &str, location: LineColumn) -> usize {
let mut offset = 0;

View file

@ -165,7 +165,14 @@ impl Writer<'_> {
match &field.content {
ContentField::ManExpr(exp) => {
let out = prettyplease::unparse_expr(exp);
write!(self.out, "{name}: {out}")?;
let mut lines = out.split('\n').peekable();
let first = lines.next().unwrap();
write!(self.out, "{name}: {first}")?;
for line in lines {
self.out.new_line()?;
self.out.indented_tab()?;
write!(self.out, "{line}")?;
}
}
ContentField::Formatted(s) => {
write!(

View file

@ -216,7 +216,26 @@ impl Writer<'_> {
}
ElementAttr::AttrExpression { name, value } => {
let out = prettyplease::unparse_expr(value);
write!(self.out, "{name}: {out}")?;
let mut lines = out.split('\n').peekable();
let first = lines.next().unwrap();
// a one-liner for whatever reason
// Does not need a new line
if lines.peek().is_none() {
write!(self.out, "{name}: {first}")?;
} else {
writeln!(self.out, "{name}: {first}")?;
while let Some(line) = lines.next() {
self.out.indented_tab()?;
write!(self.out, "{line}")?;
if lines.peek().is_none() {
write!(self.out, "")?;
} else {
writeln!(self.out)?;
}
}
}
}
ElementAttr::CustomAttrText { name, value } => {

View file

@ -27,41 +27,29 @@ impl Writer<'_> {
// If the expr is multiline, we want to collect all of its lines together and write them out properly
// This involves unshifting the first line if it's aligned
let first_line = &self.src[start.line - 1];
write!(
self.out,
"{}",
&first_line[start.column - 1..first_line.len()].trim()
)?;
write!(self.out, "{}", &first_line[start.column - 1..].trim_start())?;
let first_prefix = &self.src[start.line - 1][..start.column];
let offset = match first_prefix.trim() {
"" => 0,
_ => first_prefix
.chars()
.rev()
.take_while(|c| c.is_whitespace())
.count() as isize,
};
let prev_block_indent_level = crate::leading_whitespaces(first_line) / 4;
for (id, line) in self.src[start.line..end.line].iter().enumerate() {
writeln!(self.out)?;
// trim the leading whitespace
let line = match id {
x if x == (end.line - start.line) - 1 => &line[..end.column],
_ => line,
// check if this is the last line
let line = {
if id == (end.line - start.line) - 1 {
&line[..end.column]
} else {
line
}
};
if offset < 0 {
for _ in 0..-offset {
write!(self.out, " ")?;
}
// trim the leading whitespace
let previous_indent = crate::leading_whitespaces(line) / 4;
let offset = previous_indent.saturating_sub(prev_block_indent_level);
let required_indent = self.out.indent + offset;
self.out.write_tabs(required_indent)?;
let line = line.trim_start();
write!(self.out, "{line}")?;
} else {
let offset = offset as usize;
let right = &line[offset..];
write!(self.out, "{right}")?;
}
}
Ok(())

View file

@ -58,7 +58,7 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
let mut writer = Writer::new(contents);
// Dont parse nested macros
// Don't parse nested macros
let mut end_span = LineColumn { column: 0, line: 0 };
for item in macros {
let macro_path = &item.path.segments[0].ident;
@ -68,16 +68,11 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
continue;
}
// item.parse_body::<CallBody>();
let body = item.parse_body::<CallBody>().unwrap();
let rsx_start = macro_path.span().start();
writer.out.indent = &writer.src[rsx_start.line - 1]
.chars()
.take_while(|c| *c == ' ')
.count()
/ 4;
writer.out.indent = leading_whitespaces(writer.src[rsx_start.line - 1]) / 4;
write_body(&mut writer, &body);
@ -231,3 +226,14 @@ pub(crate) fn write_ifmt(input: &IfmtInput, writable: &mut impl Write) -> std::f
let display = DisplayIfmt(input);
write!(writable, "{}", display)
}
pub fn leading_whitespaces(input: &str) -> usize {
input
.chars()
.map_while(|c| match c {
' ' => Some(1),
'\t' => Some(4),
_ => None,
})
.sum()
}

View file

@ -81,6 +81,8 @@ 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 = []

View file

@ -12,7 +12,6 @@ use std::{
io::Read,
panic,
path::PathBuf,
process::Command,
time::Duration,
};
use wasm_bindgen_cli_support::Bindgen;
@ -244,49 +243,42 @@ 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()));
}
if output.status.success() {
let release_type = match config.release {
true => "release",
false => "debug",
@ -363,9 +355,13 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<()> {
.unwrap_or_else(|| PathBuf::from("dist"))
.display()
);
}
Ok(())
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>> {

View file

@ -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();

View file

@ -1,3 +1,4 @@
use crate::cfg::Platform;
#[cfg(feature = "plugin")]
use crate::plugin::PluginManager;
@ -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);

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 { "" };

View file

@ -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)]
@ -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()
});
if platform.as_str() == "desktop" {
crate::builder::build_desktop(&crate_config, true)?;
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()?;
}
}
return Ok(());
} else if platform != "web" {
return custom_error!("Unsupported platform target.");
}
let platform = self
.serve
.platform
.unwrap_or(crate_config.dioxus_config.application.default_platform);
match platform {
cfg::Platform::Web => {
// 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?;
server::web::startup(self.serve.port, crate_config.clone(), self.serve.open)
.await?;
}
cfg::Platform::Desktop => {
server::desktop::startup(crate_config.clone()).await?;
}
}
Ok(())
}

View file

@ -1,4 +1,4 @@
use crate::error::Result;
use crate::{cfg::Platform, error::Result};
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
@ -73,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")),
@ -115,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>,

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))
}
}
}

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();
})
}

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,6 +80,7 @@ pub fn print_console_info(ip: &String, port: u16, config: &CrateConfig, options:
);
}
if let Some(WebServerInfo { ip, port }) = web_info {
if config.dioxus_config.web.https.enabled == Some(true) {
println!(
"\t> Local : {}",
@ -91,6 +102,7 @@ pub fn print_console_info(ip: &String, port: u16, config: &CrateConfig, options:
);
println!("\t> HTTPS : {}", "Disabled".to_string().red());
}
}
println!();
println!("\t> Profile : {}", profile.green());
println!("\t> Hot Reload : {}", hot_reload.cyan());

View file

@ -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,
}

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)
}

View file

@ -33,8 +33,6 @@ log = { workspace = true }
# Serialize the Edits for use in Webview/Liveview instances
serde = { version = "1", features = ["derive"], optional = true }
bumpslab = { version = "0.2.0" }
[dev-dependencies]
tokio = { workspace = true, features = ["full"] }
dioxus = { workspace = true }

View file

@ -91,21 +91,21 @@ impl VirtualDom {
// Note: This will not remove any ids from the arena
pub(crate) fn drop_scope(&mut self, id: ScopeId, recursive: bool) {
self.dirty_scopes.remove(&DirtyScope {
height: self.scopes[id].height,
height: self.scopes[id.0].height,
id,
});
self.ensure_drop_safety(id);
if recursive {
if let Some(root) = self.scopes[id].try_root_node() {
if let Some(root) = self.scopes[id.0].try_root_node() {
if let RenderReturn::Ready(node) = unsafe { root.extend_lifetime_ref() } {
self.drop_scope_inner(node)
}
}
}
let scope = &mut self.scopes[id];
let scope = &mut self.scopes[id.0];
// Drop all the hooks once the children are dropped
// this means we'll drop hooks bottom-up
@ -116,7 +116,7 @@ impl VirtualDom {
scope.tasks.remove(task_id);
}
self.scopes.remove(id);
self.scopes.remove(id.0);
}
fn drop_scope_inner(&mut self, node: &VNode) {
@ -137,7 +137,7 @@ impl VirtualDom {
/// Descend through the tree, removing any borrowed props and listeners
pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) {
let scope = &self.scopes[scope_id];
let scope = &self.scopes[scope_id.0];
// make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we
// run the hooks (which hold an &mut Reference)

View file

@ -88,8 +88,7 @@ impl<'b> VirtualDom {
}
// Intialize the root nodes slice
node.root_ids
.intialize(vec![ElementId(0); node.template.get().roots.len()].into_boxed_slice());
*node.root_ids.borrow_mut() = vec![ElementId(0); node.template.get().roots.len()];
// The best renderers will have templates prehydrated and registered
// Just in case, let's create the template using instructions anyways
@ -328,7 +327,7 @@ impl<'b> VirtualDom {
fn load_template_root(&mut self, template: &VNode, root_idx: usize) -> ElementId {
// Get an ID for this root since it's a real root
let this_id = self.next_root(template, root_idx);
template.root_ids.set(root_idx, this_id);
template.root_ids.borrow_mut()[root_idx] = this_id;
self.mutations.push(LoadTemplate {
name: template.template.get().name,

View file

@ -15,7 +15,7 @@ use DynamicNode::*;
impl<'b> VirtualDom {
pub(super) fn diff_scope(&mut self, scope: ScopeId) {
let scope_state = &mut self.scopes[scope];
let scope_state = &mut self.scopes[scope.0];
self.scope_stack.push(scope);
unsafe {
@ -129,12 +129,14 @@ impl<'b> VirtualDom {
});
// Make sure the roots get transferred over while we're here
right_template.root_ids.transfer(&left_template.root_ids);
*right_template.root_ids.borrow_mut() = left_template.root_ids.borrow().clone();
let root_ids = right_template.root_ids.borrow();
// Update the node refs
for i in 0..right_template.root_ids.len() {
if let Some(root_id) = right_template.root_ids.get(i) {
self.update_template(root_id, right_template);
for i in 0..root_ids.len() {
if let Some(root_id) = root_ids.get(i) {
self.update_template(*root_id, right_template);
}
}
}
@ -189,7 +191,7 @@ impl<'b> VirtualDom {
right.scope.set(Some(scope_id));
// copy out the box for both
let old = self.scopes[scope_id].props.as_ref();
let old = self.scopes[scope_id.0].props.as_ref();
let new: Box<dyn AnyProps> = right.props.take().unwrap();
let new: Box<dyn AnyProps> = unsafe { std::mem::transmute(new) };
@ -201,14 +203,14 @@ impl<'b> VirtualDom {
}
// First, move over the props from the old to the new, dropping old props in the process
self.scopes[scope_id].props = Some(new);
self.scopes[scope_id.0].props = Some(new);
// Now run the component and diff it
self.run_scope(scope_id);
self.diff_scope(scope_id);
self.dirty_scopes.remove(&DirtyScope {
height: self.scopes[scope_id].height,
height: self.scopes[scope_id.0].height,
id: scope_id,
});
}
@ -686,7 +688,7 @@ impl<'b> VirtualDom {
Some(node) => node,
None => {
self.mutations.push(Mutation::PushRoot {
id: node.root_ids.get(idx).unwrap(),
id: node.root_ids.borrow()[idx],
});
return 1;
}
@ -712,7 +714,7 @@ impl<'b> VirtualDom {
Component(comp) => {
let scope = comp.scope.get().unwrap();
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
RenderReturn::Ready(node) => self.push_all_real_nodes(node),
RenderReturn::Aborted(_node) => todo!(),
}
@ -821,7 +823,7 @@ impl<'b> VirtualDom {
if let Some(dy) = node.dynamic_root(idx) {
self.remove_dynamic_node(dy, gen_muts);
} else {
let id = node.root_ids.get(idx).unwrap();
let id = node.root_ids.borrow()[idx];
if gen_muts {
self.mutations.push(Mutation::Remove { id });
}
@ -913,13 +915,13 @@ impl<'b> VirtualDom {
.expect("VComponents to always have a scope");
// Remove the component from the dom
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
RenderReturn::Ready(t) => self.remove_node(t, gen_muts),
RenderReturn::Aborted(placeholder) => self.remove_placeholder(placeholder, gen_muts),
};
// Restore the props back to the vcomponent in case it gets rendered again
let props = self.scopes[scope].props.take();
let props = self.scopes[scope.0].props.take();
*comp.props.borrow_mut() = unsafe { std::mem::transmute(props) };
// Now drop all the resouces
@ -928,13 +930,13 @@ impl<'b> VirtualDom {
fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId {
match node.dynamic_root(0) {
None => node.root_ids.get(0).unwrap(),
None => node.root_ids.borrow()[0],
Some(Text(t)) => t.id.get().unwrap(),
Some(Fragment(t)) => self.find_first_element(&t[0]),
Some(Placeholder(t)) => t.id.get().unwrap(),
Some(Component(comp)) => {
let scope = comp.scope.get().unwrap();
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
RenderReturn::Ready(t) => self.find_first_element(t),
_ => todo!("cannot handle nonstandard nodes"),
}
@ -944,13 +946,13 @@ impl<'b> VirtualDom {
fn find_last_element(&self, node: &'b VNode<'b>) -> ElementId {
match node.dynamic_root(node.template.get().roots.len() - 1) {
None => node.root_ids.last().unwrap(),
None => *node.root_ids.borrow().last().unwrap(),
Some(Text(t)) => t.id.get().unwrap(),
Some(Fragment(t)) => self.find_last_element(t.last().unwrap()),
Some(Placeholder(t)) => t.id.get().unwrap(),
Some(Component(comp)) => {
let scope = comp.scope.get().unwrap();
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
RenderReturn::Ready(t) => self.find_last_element(t),
_ => todo!("cannot handle nonstandard nodes"),
}

View file

@ -5,7 +5,7 @@ use bumpalo::boxed::Box as BumpBox;
use bumpalo::Bump;
use std::{
any::{Any, TypeId},
cell::{Cell, RefCell, UnsafeCell},
cell::{Cell, RefCell},
fmt::{Arguments, Debug},
};
@ -54,7 +54,7 @@ pub struct VNode<'a> {
/// The IDs for the roots of this template - to be used when moving the template around and removing it from
/// the actual Dom
pub root_ids: BoxedCellSlice,
pub root_ids: RefCell<Vec<ElementId>>,
/// The dynamic parts of the template
pub dynamic_nodes: &'a [DynamicNode<'a>],
@ -63,112 +63,13 @@ pub struct VNode<'a> {
pub dynamic_attrs: &'a [Attribute<'a>],
}
// Saftey: There is no way to get references to the internal data of this struct so no refrences will be invalidated by mutating the data with a immutable reference (The same principle behind Cell)
#[derive(Debug, Default)]
pub struct BoxedCellSlice(UnsafeCell<Option<Box<[ElementId]>>>);
impl Clone for BoxedCellSlice {
fn clone(&self) -> Self {
Self(UnsafeCell::new(unsafe { (*self.0.get()).clone() }))
}
}
impl BoxedCellSlice {
pub fn last(&self) -> Option<ElementId> {
unsafe {
(*self.0.get())
.as_ref()
.and_then(|inner| inner.as_ref().last().copied())
}
}
pub fn get(&self, idx: usize) -> Option<ElementId> {
unsafe {
(*self.0.get())
.as_ref()
.and_then(|inner| inner.as_ref().get(idx).copied())
}
}
pub unsafe fn get_unchecked(&self, idx: usize) -> Option<ElementId> {
(*self.0.get())
.as_ref()
.and_then(|inner| inner.as_ref().get(idx).copied())
}
pub fn set(&self, idx: usize, new: ElementId) {
unsafe {
if let Some(inner) = &mut *self.0.get() {
inner[idx] = new;
}
}
}
pub fn intialize(&self, contents: Box<[ElementId]>) {
unsafe {
*self.0.get() = Some(contents);
}
}
pub fn transfer(&self, other: &Self) {
unsafe {
*self.0.get() = (*other.0.get()).clone();
}
}
pub fn take_from(&self, other: &Self) {
unsafe {
*self.0.get() = (*other.0.get()).take();
}
}
pub fn len(&self) -> usize {
unsafe {
(*self.0.get())
.as_ref()
.map(|inner| inner.len())
.unwrap_or(0)
}
}
}
impl<'a> IntoIterator for &'a BoxedCellSlice {
type Item = ElementId;
type IntoIter = BoxedCellSliceIter<'a>;
fn into_iter(self) -> Self::IntoIter {
BoxedCellSliceIter {
index: 0,
borrow: self,
}
}
}
pub struct BoxedCellSliceIter<'a> {
index: usize,
borrow: &'a BoxedCellSlice,
}
impl Iterator for BoxedCellSliceIter<'_> {
type Item = ElementId;
fn next(&mut self) -> Option<Self::Item> {
let result = self.borrow.get(self.index);
if result.is_some() {
self.index += 1;
}
result
}
}
impl<'a> VNode<'a> {
/// Create a template with no nodes that will be skipped over during diffing
pub fn empty() -> Element<'a> {
Some(VNode {
key: None,
parent: None,
root_ids: BoxedCellSlice::default(),
root_ids: Default::default(),
dynamic_nodes: &[],
dynamic_attrs: &[],
template: Cell::new(Template {

View file

@ -20,7 +20,7 @@ impl VirtualDom {
// If the task completes...
if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() {
// Remove it from the scope so we dont try to double drop it when the scope dropes
let scope = &self.scopes[task.scope];
let scope = &self.scopes[task.scope.0];
scope.spawned_tasks.borrow_mut().remove(&id);
// Remove it from the scheduler

View file

@ -16,9 +16,9 @@ impl VirtualDom {
let parent = self.acquire_current_scope_raw();
let entry = self.scopes.vacant_entry();
let height = unsafe { parent.map(|f| (*f).height + 1).unwrap_or(0) };
let id = entry.key();
let id = ScopeId(entry.key());
entry.insert(ScopeState {
entry.insert(Box::new(ScopeState {
parent,
id,
height,
@ -35,13 +35,13 @@ impl VirtualDom {
shared_contexts: Default::default(),
borrowed_props: Default::default(),
attributes_to_drop: Default::default(),
})
}))
}
fn acquire_current_scope_raw(&self) -> Option<*const ScopeState> {
let id = self.scope_stack.last().copied()?;
let scope = self.scopes.get(id)?;
Some(scope)
let scope = self.scopes.get(id.0)?;
Some(scope.as_ref())
}
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn {
@ -51,9 +51,9 @@ impl VirtualDom {
self.ensure_drop_safety(scope_id);
let new_nodes = unsafe {
self.scopes[scope_id].previous_frame().bump_mut().reset();
self.scopes[scope_id.0].previous_frame().bump_mut().reset();
let scope = &self.scopes[scope_id];
let scope = &self.scopes[scope_id.0];
scope.suspended.set(false);
scope.hook_idx.set(0);
@ -65,7 +65,7 @@ impl VirtualDom {
props.render(scope).extend_lifetime()
};
let scope = &self.scopes[scope_id];
let scope = &self.scopes[scope_id.0];
// We write on top of the previous frame and then make it the current by pushing the generation forward
let frame = scope.previous_frame();
@ -83,13 +83,13 @@ impl VirtualDom {
id: scope.id,
});
if matches!(allocated, RenderReturn::Aborted(_)) {
if scope.suspended.get() {
if matches!(allocated, RenderReturn::Aborted(_)) {
self.suspended_scopes.insert(scope.id);
}
} else if !self.suspended_scopes.is_empty() {
_ = self.suspended_scopes.remove(&scope.id);
}
}
// rebind the lifetime now that its stored internally
unsafe { allocated.extend_lifetime_ref() }

View file

@ -9,15 +9,12 @@ use crate::{
AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId,
};
use bumpalo::{boxed::Box as BumpBox, Bump};
use bumpslab::{BumpSlab, Slot};
use rustc_hash::FxHashSet;
use slab::{Slab, VacantEntry};
use std::{
any::{Any, TypeId},
cell::{Cell, RefCell, UnsafeCell},
fmt::{Arguments, Debug},
future::Future,
ops::{Index, IndexMut},
rc::Rc,
sync::Arc,
};
@ -65,95 +62,6 @@ impl<'a, T> std::ops::Deref for Scoped<'a, T> {
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
pub struct ScopeId(pub usize);
/// A thin wrapper around a BumpSlab that uses ids to index into the slab.
pub(crate) struct ScopeSlab {
slab: BumpSlab<ScopeState>,
// a slab of slots of stable pointers to the ScopeState in the bump slab
entries: Slab<Slot<'static, ScopeState>>,
}
impl Drop for ScopeSlab {
fn drop(&mut self) {
// Bump slab doesn't drop its contents, so we need to do it manually
for slot in self.entries.drain() {
self.slab.remove(slot);
}
}
}
impl Default for ScopeSlab {
fn default() -> Self {
Self {
slab: BumpSlab::new(),
entries: Slab::new(),
}
}
}
impl ScopeSlab {
pub(crate) fn get(&self, id: ScopeId) -> Option<&ScopeState> {
self.entries.get(id.0).map(|slot| unsafe { &*slot.ptr() })
}
pub(crate) fn get_mut(&mut self, id: ScopeId) -> Option<&mut ScopeState> {
self.entries
.get(id.0)
.map(|slot| unsafe { &mut *slot.ptr_mut() })
}
pub(crate) fn vacant_entry(&mut self) -> ScopeSlabEntry {
let entry = self.entries.vacant_entry();
ScopeSlabEntry {
slab: &mut self.slab,
entry,
}
}
pub(crate) fn remove(&mut self, id: ScopeId) {
self.slab.remove(self.entries.remove(id.0));
}
pub(crate) fn contains(&self, id: ScopeId) -> bool {
self.entries.contains(id.0)
}
pub(crate) fn iter(&self) -> impl Iterator<Item = &ScopeState> {
self.entries.iter().map(|(_, slot)| unsafe { &*slot.ptr() })
}
}
pub(crate) struct ScopeSlabEntry<'a> {
slab: &'a mut BumpSlab<ScopeState>,
entry: VacantEntry<'a, Slot<'static, ScopeState>>,
}
impl<'a> ScopeSlabEntry<'a> {
pub(crate) fn key(&self) -> ScopeId {
ScopeId(self.entry.key())
}
pub(crate) fn insert(self, scope: ScopeState) -> &'a ScopeState {
let slot = self.slab.push(scope);
// this is safe because the slot is only ever accessed with the lifetime of the borrow of the slab
let slot = unsafe { std::mem::transmute(slot) };
let entry = self.entry.insert(slot);
unsafe { &*entry.ptr() }
}
}
impl Index<ScopeId> for ScopeSlab {
type Output = ScopeState;
fn index(&self, id: ScopeId) -> &Self::Output {
self.get(id).unwrap()
}
}
impl IndexMut<ScopeId> for ScopeSlab {
fn index_mut(&mut self, id: ScopeId) -> &mut Self::Output {
self.get_mut(id).unwrap()
}
}
/// A component's state separate from its props.
///
/// This struct exists to provide a common interface for all scopes without relying on generics.

View file

@ -5,7 +5,7 @@
use crate::{
any_props::VProps,
arena::{ElementId, ElementRef},
innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg, ScopeSlab},
innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
mutations::Mutation,
nodes::RenderReturn,
nodes::{Template, TemplateId},
@ -176,7 +176,7 @@ use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
pub struct VirtualDom {
// Maps a template path to a map of byteindexes to templates
pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template<'static>>>,
pub(crate) scopes: ScopeSlab,
pub(crate) scopes: Slab<Box<ScopeState>>,
pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
pub(crate) scheduler: Rc<Scheduler>,
@ -281,14 +281,14 @@ impl VirtualDom {
///
/// This is useful for inserting or removing contexts from a scope, or rendering out its root node
pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
self.scopes.get(id)
self.scopes.get(id.0).map(|f| f.as_ref())
}
/// Get the single scope at the top of the VirtualDom tree that will always be around
///
/// This scope has a ScopeId of 0 and is the root of the tree
pub fn base_scope(&self) -> &ScopeState {
self.scopes.get(ScopeId(0)).unwrap()
self.get_scope(ScopeId(0)).unwrap()
}
/// Build the virtualdom with a global context inserted into the base scope
@ -303,7 +303,7 @@ impl VirtualDom {
///
/// Whenever the VirtualDom "works", it will re-render this scope
pub fn mark_dirty(&mut self, id: ScopeId) {
if let Some(scope) = self.scopes.get(id) {
if let Some(scope) = self.get_scope(id) {
let height = scope.height;
self.dirty_scopes.insert(DirtyScope { height, id });
}
@ -496,7 +496,7 @@ impl VirtualDom {
pub fn replace_template(&mut self, template: Template<'static>) {
self.register_template_first_byte_index(template);
// iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine
for scope in self.scopes.iter() {
for (_, scope) in self.scopes.iter() {
if let Some(RenderReturn::Ready(sync)) = scope.try_root_node() {
if sync.template.get().name.rsplit_once(':').unwrap().0
== template.name.rsplit_once(':').unwrap().0
@ -571,12 +571,15 @@ impl VirtualDom {
/// The mutations will be thrown out, so it's best to use this method for things like SSR that have async content
pub async fn wait_for_suspense(&mut self) {
loop {
// println!("waiting for suspense {:?}", self.suspended_scopes);
if self.suspended_scopes.is_empty() {
return;
}
// println!("waiting for suspense");
self.wait_for_work().await;
// println!("Rendered immediately");
_ = self.render_immediate();
}
}
@ -598,7 +601,7 @@ impl VirtualDom {
self.dirty_scopes.remove(&dirty);
// If the scope doesn't exist for whatever reason, then we should skip it
if !self.scopes.contains(dirty.id) {
if !self.scopes.contains(dirty.id.0) {
continue;
}

View file

@ -1,3 +1,5 @@
#![cfg(not(miri))]
use dioxus::prelude::Props;
use dioxus_core::*;
use std::{cell::Cell, collections::HashSet};
@ -314,6 +316,7 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
}
// test for panics when creating random nodes and templates
#[cfg(not(miri))]
#[test]
fn create() {
for _ in 0..1000 {
@ -325,6 +328,7 @@ fn create() {
// test for panics when diffing random nodes
// This test will change the template every render which is not very realistic, but it helps stress the system
#[cfg(not(miri))]
#[test]
fn diff() {
for _ in 0..100000 {

View file

@ -1,9 +1,13 @@
use dioxus::prelude::*;
#[tokio::test]
async fn it_works() {
#[test]
fn it_works() {
// wait just a moment, not enough time for the boundary to resolve
tokio::runtime::Builder::new_current_thread()
.build()
.unwrap()
.block_on(async {
let mut dom = VirtualDom::new(app);
_ = dom.rebuild();
dom.wait_for_suspense().await;
@ -12,6 +16,7 @@ async fn it_works() {
assert_eq!(out, "<div>Waiting for... child</div>");
dbg!(out);
});
}
fn app(cx: Scope) -> Element {

View file

@ -1,10 +1,11 @@
//! Verify that tasks get polled by the virtualdom properly, and that we escape wait_for_work safely
use dioxus::prelude::*;
use std::time::Duration;
use std::{sync::atomic::AtomicUsize, time::Duration};
static mut POLL_COUNT: usize = 0;
static POLL_COUNT: AtomicUsize = AtomicUsize::new(0);
#[cfg(not(miri))]
#[tokio::test]
async fn it_works() {
let mut dom = VirtualDom::new(app);
@ -18,7 +19,10 @@ async fn it_works() {
// By the time the tasks are finished, we should've accumulated ticks from two tasks
// Be warned that by setting the delay to too short, tokio might not schedule in the tasks
assert_eq!(unsafe { POLL_COUNT }, 135);
assert_eq!(
POLL_COUNT.fetch_add(0, std::sync::atomic::Ordering::Relaxed),
135
);
}
fn app(cx: Scope) -> Element {
@ -26,14 +30,14 @@ fn app(cx: Scope) -> Element {
cx.spawn(async {
for x in 0..10 {
tokio::time::sleep(Duration::from_micros(50)).await;
unsafe { POLL_COUNT += x }
POLL_COUNT.fetch_add(x, std::sync::atomic::Ordering::Relaxed);
}
});
cx.spawn(async {
for x in 0..10 {
tokio::time::sleep(Duration::from_micros(25)).await;
unsafe { POLL_COUNT += x * 2 }
POLL_COUNT.fetch_add(x * 2, std::sync::atomic::Ordering::Relaxed);
}
});
});

View file

@ -14,7 +14,33 @@ pub(crate) struct FileDialogRequest {
pub bubbles: bool,
}
fn get_file_event_for_folder(request: &FileDialogRequest, dialog: rfd::FileDialog) -> Vec<PathBuf> {
#[cfg(not(any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)))]
pub(crate) fn get_file_event(_request: &FileDialogRequest) -> Vec<PathBuf> {
vec![]
}
#[cfg(any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
pub(crate) fn get_file_event(request: &FileDialogRequest) -> Vec<PathBuf> {
fn get_file_event_for_folder(
request: &FileDialogRequest,
dialog: rfd::FileDialog,
) -> Vec<PathBuf> {
if request.multiple {
dialog.pick_folders().into_iter().flatten().collect()
} else {
@ -50,29 +76,6 @@ fn get_file_event_for_file(
files
}
#[cfg(not(any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
)))]
pub(crate) fn get_file_event(_request: &FileDialogRequest) -> Vec<PathBuf> {
vec![]
}
#[cfg(any(
target_os = "windows",
target_os = "macos",
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
pub(crate) fn get_file_event(request: &FileDialogRequest) -> Vec<PathBuf> {
let dialog = rfd::FileDialog::new();
if request.directory {

View file

@ -20,7 +20,7 @@ fn app(cx: Scope) -> Element {
background_color: "hsl({hue}, 70%, {brightness}%)",
onmousemove: move |evt| {
if let RenderReturn::Ready(node) = cx.root_node() {
if let Some(id) = node.root_ids.get(0){
if let Some(id) = node.root_ids.borrow().get(0).cloned() {
let node = tui_query.get(mapping.get_node_id(id).unwrap());
let Size{width, height} = node.size().unwrap();
let pos = evt.inner().element_coordinates();

View file

@ -1,12 +1,12 @@
{
"name": "dioxus",
"version": "0.0.1",
"version": "0.0.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "dioxus",
"version": "0.0.1",
"version": "0.0.2",
"license": "MIT",
"dependencies": {
"dioxus-ext": "./pkg",
@ -3720,9 +3720,9 @@
"dev": true
},
"node_modules/word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
@ -6444,9 +6444,9 @@
"dev": true
},
"word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"dev": true
},
"wrappy": {

View file

@ -1,12 +1,8 @@
#![allow(missing_docs)]
use dioxus_core::{ScopeState, TaskId};
use std::{
any::Any,
cell::{Cell, RefCell},
future::{Future, IntoFuture},
rc::Rc,
sync::Arc,
};
use std::{any::Any, cell::Cell, future::Future, rc::Rc, sync::Arc};
use crate::{use_state, UseState};
/// A future that resolves to a value.
///
@ -31,43 +27,30 @@ where
F: Future<Output = T> + 'static,
D: UseFutureDep,
{
let val = use_state(cx, || None);
let state = cx.use_hook(move || UseFuture {
update: cx.schedule_update(),
needs_regen: Cell::new(true),
values: Default::default(),
task: Cell::new(None),
state: val.clone(),
task: Default::default(),
dependencies: Vec::new(),
waker: Default::default(),
});
*state.waker.borrow_mut() = None;
if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen.get() {
// We don't need regen anymore
state.needs_regen.set(false);
// kill the old one, if it exists
if let Some(task) = state.task.take() {
cx.remove_future(task);
}
// Create the new future
let fut = future(dependencies.out());
// Clone in our cells
let values = state.values.clone();
let schedule_update = state.update.clone();
let waker = state.waker.clone();
// Cancel the current future
if let Some(current) = state.task.take() {
cx.remove_future(current);
}
let val = val.clone();
let task = state.task.clone();
state.task.set(Some(cx.push_future(async move {
let res = fut.await;
values.borrow_mut().push(Box::leak(Box::new(res)));
// if there's a waker, we dont re-render the component. Instead we just progress that future
match waker.borrow().as_ref() {
Some(waker) => waker.wake_by_ref(),
None => schedule_update(),
}
val.set(Some(fut.await));
task.take();
})));
}
@ -80,21 +63,12 @@ pub enum FutureState<'a, T> {
Regenerating(&'a T), // the old value
}
pub struct UseFuture<T> {
pub struct UseFuture<T: 'static> {
update: Arc<dyn Fn()>,
needs_regen: Cell<bool>,
task: Cell<Option<TaskId>>,
task: Rc<Cell<Option<TaskId>>>,
dependencies: Vec<Box<dyn Any>>,
waker: Rc<RefCell<Option<std::task::Waker>>>,
values: Rc<RefCell<Vec<*mut T>>>,
}
impl<T> Drop for UseFuture<T> {
fn drop(&mut self) {
for value in self.values.take().into_iter() {
drop(unsafe { Box::from_raw(value) })
}
}
state: UseState<Option<T>>,
}
pub enum UseFutureState<'a, T> {
@ -120,30 +94,16 @@ impl<T> UseFuture<T> {
}
}
// clears the value in the future slot without starting the future over
pub fn clear(&self) -> Option<T> {
todo!()
// (self.update)();
// self.slot.replace(None)
}
// Manually set the value in the future slot without starting the future over
pub fn set(&self, _new_value: T) {
// self.slot.set(Some(new_value));
// self.needs_regen.set(true);
// (self.update)();
todo!()
pub fn set(&self, new_value: T) {
self.state.set(Some(new_value));
}
/// Return any value, even old values if the future has not yet resolved.
///
/// If the future has never completed, the returned value will be `None`.
pub fn value(&self) -> Option<&T> {
self.values
.borrow_mut()
.last()
.cloned()
.map(|x| unsafe { &*x })
self.state.current_val.as_ref().as_ref()
}
/// Get the ID of the future in Dioxus' internal scheduler
@ -169,35 +129,6 @@ impl<T> UseFuture<T> {
}
}
impl<'a, T> IntoFuture for &'a UseFuture<T> {
type Output = &'a T;
type IntoFuture = UseFutureAwait<'a, T>;
fn into_future(self) -> Self::IntoFuture {
UseFutureAwait { hook: self }
}
}
pub struct UseFutureAwait<'a, T> {
hook: &'a UseFuture<T>,
}
impl<'a, T> Future for UseFutureAwait<'a, T> {
type Output = &'a T;
fn poll(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
match self.hook.values.borrow_mut().last().cloned() {
Some(value) => std::task::Poll::Ready(unsafe { &*value }),
None => {
self.hook.waker.replace(Some(cx.waker().clone()));
std::task::Poll::Pending
}
}
}
}
pub trait UseFutureDep: Sized + Clone {
type Out;
fn out(&self) -> Self::Out;
@ -343,10 +274,6 @@ mod tests {
let blah = "asd";
});
let g2 = a.await;
let g = fut.await;
todo!()
}
}

View file

@ -236,7 +236,7 @@ pub(crate) fn create_error_type(
match segment {
RouteSegment::Static(index) => {
error_variants.push(quote! { #error_name(String) });
display_match.push(quote! { Self::#error_name(found) => write!(f, "Static segment '{}' did not match instead found '{found}'", #index)? });
display_match.push(quote! { Self::#error_name(found) => write!(f, "Static segment '{}' did not match instead found '{}'", #index, found)? });
}
RouteSegment::Dynamic(ident, ty) => {
let missing_error = segment.missing_error_name().unwrap();

View file

@ -90,7 +90,7 @@ impl WebsysDom {
// make sure we set the root node ids even if the node is not dynamic
set_node(
hydrated,
vnode.root_ids.get(i).ok_or(VNodeNotInitialized)?,
*vnode.root_ids.borrow().get(i).ok_or(VNodeNotInitialized)?,
current_child.clone()?,
);

View file

@ -9,23 +9,23 @@
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.34.3"
"@playwright/test": "^1.36.1"
}
},
"node_modules/@playwright/test": {
"version": "1.34.3",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.34.3.tgz",
"integrity": "sha512-zPLef6w9P6T/iT6XDYG3mvGOqOyb6eHaV9XtkunYs0+OzxBtrPAAaHotc0X+PJ00WPPnLfFBTl7mf45Mn8DBmw==",
"version": "1.36.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.36.1.tgz",
"integrity": "sha512-YK7yGWK0N3C2QInPU6iaf/L3N95dlGdbsezLya4n0ZCh3IL7VgPGxC6Gnznh9ApWdOmkJeleT2kMTcWPRZvzqg==",
"dev": true,
"dependencies": {
"@types/node": "*",
"playwright-core": "1.34.3"
"playwright-core": "1.36.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
"node": ">=16"
},
"optionalDependencies": {
"fsevents": "2.3.2"
@ -52,15 +52,15 @@
}
},
"node_modules/playwright-core": {
"version": "1.34.3",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.34.3.tgz",
"integrity": "sha512-2pWd6G7OHKemc5x1r1rp8aQcpvDh7goMBZlJv6Co5vCNLVcQJdhxRL09SGaY6HcyHH9aT4tiynZabMofVasBYw==",
"version": "1.36.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.36.1.tgz",
"integrity": "sha512-7+tmPuMcEW4xeCL9cp9KxmYpQYHKkyjwoXRnoeTowaeNat8PoBMk/HwCYhqkH2fRkshfKEOiVus/IhID2Pg8kg==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=14"
"node": ">=16"
}
}
}

View file

@ -12,6 +12,6 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.34.3"
"@playwright/test": "^1.36.1"
}
}

File diff suppressed because one or more lines are too long

View file

@ -12,7 +12,7 @@ const path = require("path");
* @see https://playwright.dev/docs/test-configuration
*/
module.exports = defineConfig({
testDir: "./playwright-tests",
testDir: ".",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
@ -81,16 +81,16 @@ module.exports = defineConfig({
stdout: "pipe",
},
{
cwd: path.join(process.cwd(), "playwright-tests", "web"),
command: "dx serve",
cwd: path.join(process.cwd(), "web"),
command: "cargo run --package dioxus-cli -- serve",
port: 8080,
timeout: 10 * 60 * 1000,
reuseExistingServer: !process.env.CI,
stdout: "pipe",
},
{
cwd: path.join(process.cwd(), 'playwrite-tests', 'fullstack'),
command: 'dx build --features web --release\ncargo run --release --features ssr',
cwd: path.join(process.cwd(), 'fullstack'),
command: 'cargo run --package dioxus-cli -- build --features web --release\ncargo run --release --features ssr',
port: 3333,
timeout: 10 * 60 * 1000,
reuseExistingServer: !process.env.CI,