diff --git a/.github/workflows/docs stable.yml b/.github/workflows/docs stable.yml index a2d4e2c07..069d4308a 100644 --- a/.github/workflows/docs stable.yml +++ b/.github/workflows/docs stable.yml @@ -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 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index dafe0384d..3a452e770 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -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 diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml deleted file mode 100644 index eef04d521..000000000 --- a/.github/workflows/macos.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b37e63aa4..0abb1404c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 + diff --git a/.github/workflows/miri.yml b/.github/workflows/miri.yml index 73b38dec9..b1d772512 100644 --- a/.github/workflows/miri.yml +++ b/.github/workflows/miri.yml @@ -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 diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 84891f403..41fa92fa7 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -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 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml deleted file mode 100644 index 368df1630..000000000 --- a/.github/workflows/windows.yml +++ /dev/null @@ -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 diff --git a/packages/autofmt/Cargo.toml b/packages/autofmt/Cargo.toml index c658a9f42..f17056bc5 100644 --- a/packages/autofmt/Cargo.toml +++ b/packages/autofmt/Cargo.toml @@ -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", diff --git a/packages/autofmt/src/collect_macros.rs b/packages/autofmt/src/collect_macros.rs index 6494b4bfe..10e0d185c 100644 --- a/packages/autofmt/src/collect_macros.rs +++ b/packages/autofmt/src/collect_macros.rs @@ -3,181 +3,24 @@ //! 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>) { - 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>) { - 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(¯o_.mac); - } - } - - // 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); - } - } - } - - // 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(_) => {} - _ => {} - } +struct MacroCollector<'a, 'b> { + macros: &'a mut Vec>, } -pub fn collect_from_block<'a>(block: &'a Block, macros: &mut Vec>) { - 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); - } - } - } +impl<'a, 'b> Visit<'b> for MacroCollector<'a, 'b> { + fn visit_macro(&mut self, i: &'b Macro) { + self.macros.push(i); } } -pub fn collect_from_expr<'a>(expr: &'a Expr, macros: &mut Vec>) { - // 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(¯o_.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; for _ in 1..location.line { diff --git a/packages/autofmt/src/component.rs b/packages/autofmt/src/component.rs index d5d92b79a..8cf36811d 100644 --- a/packages/autofmt/src/component.rs +++ b/packages/autofmt/src/component.rs @@ -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!( diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index c541c8f5c..dbc336401 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -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 } => { diff --git a/packages/autofmt/src/expr.rs b/packages/autofmt/src/expr.rs index ddc0892f8..b0257f8c3 100644 --- a/packages/autofmt/src/expr.rs +++ b/packages/autofmt/src/expr.rs @@ -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)?; - write!(self.out, "{line}")?; - } else { - let offset = offset as usize; - let right = &line[offset..]; - write!(self.out, "{right}")?; - } + let line = line.trim_start(); + write!(self.out, "{line}")?; } Ok(()) diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs index 7d98fb222..8ec33f0f0 100644 --- a/packages/autofmt/src/lib.rs +++ b/packages/autofmt/src/lib.rs @@ -58,7 +58,7 @@ pub fn fmt_file(contents: &str) -> Vec { 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 { continue; } - // item.parse_body::(); let body = item.parse_body::().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() +} diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index a88b6df18..960a4c98a 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -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 = [] diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index cf7ccd7b3..b25576120 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -12,7 +12,6 @@ use std::{ io::Read, panic, path::PathBuf, - process::Command, time::Duration, }; use wasm_bindgen_cli_support::Bindgen; @@ -244,128 +243,125 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { }) } -pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<()> { +pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result { 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, ©_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, ©_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> { diff --git a/packages/cli/src/cli/autoformat.rs b/packages/cli/src/cli/autoformat.rs index ea2c27148..a0c0a8066 100644 --- a/packages/cli/src/cli/autoformat.rs +++ b/packages/cli/src/cli/autoformat.rs @@ -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::>() .collect::>() @@ -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(); diff --git a/packages/cli/src/cli/build.rs b/packages/cli/src/cli/build.rs index 148a4ab76..3053a6e5a 100644 --- a/packages/cli/src/cli/build.rs +++ b/packages/cli/src/cli/build.rs @@ -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); diff --git a/packages/cli/src/cli/cfg.rs b/packages/cli/src/cli/cfg.rs index 30ca89ce9..edc32f346 100644 --- a/packages/cli/src/cli/cfg.rs +++ b/packages/cli/src/cli/cfg.rs @@ -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, /// Build platform: support Web & Desktop [default: "default_platform"] - #[clap(long)] - pub platform: Option, + #[clap(long, value_enum)] + pub platform: Option, /// Space separated list of features to activate #[clap(long)] @@ -69,8 +72,8 @@ pub struct ConfigOptsServe { pub profile: Option, /// Build platform: support Web & Desktop [default: "default_platform"] - #[clap(long)] - pub platform: Option, + #[clap(long, value_enum)] + pub platform: Option, /// Build with hot reloading rsx [default: false] #[clap(long)] @@ -88,6 +91,16 @@ pub struct ConfigOptsServe { pub features: Option>, } +#[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 { "" }; diff --git a/packages/cli/src/cli/serve.rs b/packages/cli/src/cli/serve.rs index e50eb0f38..433d611b7 100644 --- a/packages/cli/src/cli/serve.rs +++ b/packages/cli/src/cli/serve.rs @@ -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() - }); + 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(()) } diff --git a/packages/cli/src/config.rs b/packages/cli/src/config.rs index 05aca0d35..e46b4f0fe 100644 --- a/packages/cli/src/config.rs +++ b/packages/cli/src/config.rs @@ -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, pub asset_dir: Option, diff --git a/packages/cli/src/server/desktop/mod.rs b/packages/cli/src/server/desktop/mod.rs new file mode 100644 index 000000000..9b79bdea9 --- /dev/null +++ b/packages/cli/src/server/desktop/mod.rs @@ -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 = 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::::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)) + } + } +} diff --git a/packages/cli/src/server/mod.rs b/packages/cli/src/server/mod.rs index 3bd434005..19dc44b75 100644 --- a/packages/cli/src/server/mod.rs +++ b/packages/cli/src/server/mod.rs @@ -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 { - 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::::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> { - 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, - hot_reload: Option>, -) -> Result { - // 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| 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, -) -> 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 Result + Send + 'static>( + build_with: F, config: &CrateConfig, - port: u16, - watcher_ip: String, - reload_tx: Sender<()>, + web_info: Option, ) -> Result { - 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 Result + Send + 'static>( config: &CrateConfig, - port: u16, - watcher_ip: String, hot_reload_tx: Sender>, file_map: Arc>>, - build_manager: Arc, + build_with: F, + web_info: Option, ) -> Result { // 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 { - 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>, - Extension(state): Extension>, -) -> 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(); - }) -} diff --git a/packages/cli/src/server/output.rs b/packages/cli/src/server/output.rs index 4c6e1f3b2..ce0f55064 100644 --- a/packages/cli/src/server/output.rs +++ b/packages/cli/src/server/output.rs @@ -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, +) { 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()); diff --git a/packages/cli/src/server/hot_reload.rs b/packages/cli/src/server/web/hot_reload.rs similarity index 96% rename from packages/cli/src/server/hot_reload.rs rename to packages/cli/src/server/web/hot_reload.rs index 167e712af..49cce38d4 100644 --- a/packages/cli/src/server/hot_reload.rs +++ b/packages/cli/src/server/web/hot_reload.rs @@ -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>, - pub build_manager: Arc, pub file_map: Arc>>, pub watcher_config: CrateConfig, } diff --git a/packages/cli/src/server/web/mod.rs b/packages/cli/src/server/web/mod.rs new file mode 100644 index 000000000..e08d2297a --- /dev/null +++ b/packages/cli/src/server/web/mod.rs @@ -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::::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> { + 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, + hot_reload: Option>, +) -> Result { + // 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| 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, +) -> 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 { + 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>, + Extension(state): Extension>, +) -> 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 { + 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) +} diff --git a/packages/cli/src/server/proxy.rs b/packages/cli/src/server/web/proxy.rs similarity index 100% rename from packages/cli/src/server/proxy.rs rename to packages/cli/src/server/web/proxy.rs diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index a077a7379..3b85a8d2d 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -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 } diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 04ffb50d1..b6254136d 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -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) diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index 8efdf13e4..6159c882b 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -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, diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index cfda18505..7865224aa 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -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 = right.props.take().unwrap(); let new: Box = 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"), } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 209c37c77..197550a3b 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -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>, /// 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>>); - -impl Clone for BoxedCellSlice { - fn clone(&self) -> Self { - Self(UnsafeCell::new(unsafe { (*self.0.get()).clone() })) - } -} - -impl BoxedCellSlice { - pub fn last(&self) -> Option { - unsafe { - (*self.0.get()) - .as_ref() - .and_then(|inner| inner.as_ref().last().copied()) - } - } - - pub fn get(&self, idx: usize) -> Option { - unsafe { - (*self.0.get()) - .as_ref() - .and_then(|inner| inner.as_ref().get(idx).copied()) - } - } - - pub unsafe fn get_unchecked(&self, idx: usize) -> Option { - (*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 { - 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 { diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs index 0a7dce207..b475c6751 100644 --- a/packages/core/src/scheduler/wait.rs +++ b/packages/core/src/scheduler/wait.rs @@ -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 diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index d058e34de..b298a8996 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -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,12 +83,12 @@ impl VirtualDom { id: scope.id, }); - if matches!(allocated, RenderReturn::Aborted(_)) { - if scope.suspended.get() { + 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); } + } else if !self.suspended_scopes.is_empty() { + _ = self.suspended_scopes.remove(&scope.id); } // rebind the lifetime now that its stored internally diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 33fb9aac5..9470c3d0d 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -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, - // a slab of slots of stable pointers to the ScopeState in the bump slab - entries: Slab>, -} - -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 { - self.entries.iter().map(|(_, slot)| unsafe { &*slot.ptr() }) - } -} - -pub(crate) struct ScopeSlabEntry<'a> { - slab: &'a mut BumpSlab, - 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 for ScopeSlab { - type Output = ScopeState; - fn index(&self, id: ScopeId) -> &Self::Output { - self.get(id).unwrap() - } -} - -impl IndexMut 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. diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 31a1b5912..68a4e5a1b 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -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>>, - pub(crate) scopes: ScopeSlab, + pub(crate) scopes: Slab>, pub(crate) dirty_scopes: BTreeSet, pub(crate) scheduler: Rc, @@ -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; } diff --git a/packages/core/tests/fuzzing.rs b/packages/core/tests/fuzzing.rs index 3c617dc02..f193aa2f7 100644 --- a/packages/core/tests/fuzzing.rs +++ b/packages/core/tests/fuzzing.rs @@ -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) -> 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 { diff --git a/packages/core/tests/suspense.rs b/packages/core/tests/suspense.rs index 861b609ca..7b2516447 100644 --- a/packages/core/tests/suspense.rs +++ b/packages/core/tests/suspense.rs @@ -1,17 +1,22 @@ 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 - let mut dom = VirtualDom::new(app); - _ = dom.rebuild(); - dom.wait_for_suspense().await; - let out = dioxus_ssr::pre_render(&dom); + tokio::runtime::Builder::new_current_thread() + .build() + .unwrap() + .block_on(async { + let mut dom = VirtualDom::new(app); + _ = dom.rebuild(); + dom.wait_for_suspense().await; + let out = dioxus_ssr::pre_render(&dom); - assert_eq!(out, "
Waiting for... child
"); + assert_eq!(out, "
Waiting for... child
"); - dbg!(out); + dbg!(out); + }); } fn app(cx: Scope) -> Element { diff --git a/packages/core/tests/task.rs b/packages/core/tests/task.rs index 2704c5eb2..1d190ccaf 100644 --- a/packages/core/tests/task.rs +++ b/packages/core/tests/task.rs @@ -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); } }); }); diff --git a/packages/desktop/src/file_upload.rs b/packages/desktop/src/file_upload.rs index 778c209ac..7b5f08fc3 100644 --- a/packages/desktop/src/file_upload.rs +++ b/packages/desktop/src/file_upload.rs @@ -14,42 +14,6 @@ pub(crate) struct FileDialogRequest { pub bubbles: bool, } -fn get_file_event_for_folder(request: &FileDialogRequest, dialog: rfd::FileDialog) -> Vec { - if request.multiple { - dialog.pick_folders().into_iter().flatten().collect() - } else { - dialog.pick_folder().into_iter().collect() - } -} - -fn get_file_event_for_file( - request: &FileDialogRequest, - mut dialog: rfd::FileDialog, -) -> Vec { - let filters: Vec<_> = request - .accept - .as_deref() - .unwrap_or_default() - .split(',') - .filter_map(|s| Filters::from_str(s).ok()) - .collect(); - - let file_extensions: Vec<_> = filters - .iter() - .flat_map(|f| f.as_extensions().into_iter()) - .collect(); - - dialog = dialog.add_filter("name", file_extensions.as_slice()); - - let files: Vec<_> = if request.multiple { - dialog.pick_files().into_iter().flatten().collect() - } else { - dialog.pick_file().into_iter().collect() - }; - - files -} - #[cfg(not(any( target_os = "windows", target_os = "macos", @@ -73,6 +37,45 @@ pub(crate) fn get_file_event(_request: &FileDialogRequest) -> Vec { target_os = "openbsd" ))] pub(crate) fn get_file_event(request: &FileDialogRequest) -> Vec { + fn get_file_event_for_folder( + request: &FileDialogRequest, + dialog: rfd::FileDialog, + ) -> Vec { + if request.multiple { + dialog.pick_folders().into_iter().flatten().collect() + } else { + dialog.pick_folder().into_iter().collect() + } + } + + fn get_file_event_for_file( + request: &FileDialogRequest, + mut dialog: rfd::FileDialog, + ) -> Vec { + let filters: Vec<_> = request + .accept + .as_deref() + .unwrap_or_default() + .split(',') + .filter_map(|s| Filters::from_str(s).ok()) + .collect(); + + let file_extensions: Vec<_> = filters + .iter() + .flat_map(|f| f.as_extensions().into_iter()) + .collect(); + + dialog = dialog.add_filter("name", file_extensions.as_slice()); + + let files: Vec<_> = if request.multiple { + dialog.pick_files().into_iter().flatten().collect() + } else { + dialog.pick_file().into_iter().collect() + }; + + files + } + let dialog = rfd::FileDialog::new(); if request.directory { diff --git a/packages/dioxus-tui/examples/colorpicker.rs b/packages/dioxus-tui/examples/colorpicker.rs index 1de6b8209..00f8ef7e0 100644 --- a/packages/dioxus-tui/examples/colorpicker.rs +++ b/packages/dioxus-tui/examples/colorpicker.rs @@ -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(); diff --git a/packages/extension/package-lock.json b/packages/extension/package-lock.json index 6d3a8b27f..2a51fc2e8 100644 --- a/packages/extension/package-lock.json +++ b/packages/extension/package-lock.json @@ -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": { diff --git a/packages/hooks/src/usefuture.rs b/packages/hooks/src/usefuture.rs index 18bf50310..4d90adbd4 100644 --- a/packages/hooks/src/usefuture.rs +++ b/packages/hooks/src/usefuture.rs @@ -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 + '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 { +pub struct UseFuture { update: Arc, needs_regen: Cell, - task: Cell>, + task: Rc>>, dependencies: Vec>, - waker: Rc>>, - values: Rc>>, -} - -impl Drop for UseFuture { - fn drop(&mut self) { - for value in self.values.take().into_iter() { - drop(unsafe { Box::from_raw(value) }) - } - } + state: UseState>, } pub enum UseFutureState<'a, T> { @@ -120,30 +94,16 @@ impl UseFuture { } } - // clears the value in the future slot without starting the future over - pub fn clear(&self) -> Option { - 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 UseFuture { } } -impl<'a, T> IntoFuture for &'a UseFuture { - 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, -} - -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 { - 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!() } } diff --git a/packages/router-macro/src/segment.rs b/packages/router-macro/src/segment.rs index 2f7455a3f..62dea59a7 100644 --- a/packages/router-macro/src/segment.rs +++ b/packages/router-macro/src/segment.rs @@ -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(); diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 8af7da16d..5cc9ea57e 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -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()?, ); diff --git a/package-lock.json b/playwright-tests/package-lock.json similarity index 76% rename from package-lock.json rename to playwright-tests/package-lock.json index 348b505d4..4f1b932ac 100644 --- a/package-lock.json +++ b/playwright-tests/package-lock.json @@ -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" } } } diff --git a/package.json b/playwright-tests/package.json similarity index 90% rename from package.json rename to playwright-tests/package.json index ceb38db89..31d444420 100644 --- a/package.json +++ b/playwright-tests/package.json @@ -12,6 +12,6 @@ "author": "", "license": "ISC", "devDependencies": { - "@playwright/test": "^1.34.3" + "@playwright/test": "^1.36.1" } } diff --git a/playwright-tests/playwright-report/index.html b/playwright-tests/playwright-report/index.html new file mode 100644 index 000000000..71730368d --- /dev/null +++ b/playwright-tests/playwright-report/index.html @@ -0,0 +1,62 @@ + + + + + + + + + Playwright Test Report + + + + +
+ + + + \ No newline at end of file diff --git a/playwright.config.js b/playwright-tests/playwright.config.js similarity index 89% rename from playwright.config.js rename to playwright-tests/playwright.config.js index d7f019bbd..54fe810a7 100644 --- a/playwright.config.js +++ b/playwright-tests/playwright.config.js @@ -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,