mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-23 04:33:06 +00:00
more clippy fixes
This commit is contained in:
commit
beb56b93a0
48 changed files with 1327 additions and 1415 deletions
4
.github/workflows/docs stable.yml
vendored
4
.github/workflows/docs stable.yml
vendored
|
@ -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
|
||||
|
|
4
.github/workflows/docs.yml
vendored
4
.github/workflows/docs.yml
vendored
|
@ -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
|
||||
|
|
38
.github/workflows/macos.yml
vendored
38
.github/workflows/macos.yml
vendored
|
@ -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
|
68
.github/workflows/main.yml
vendored
68
.github/workflows/main.yml
vendored
|
@ -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
|
||||
|
||||
|
|
13
.github/workflows/miri.yml
vendored
13
.github/workflows/miri.yml
vendored
|
@ -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
|
||||
|
|
32
.github/workflows/playwright.yml
vendored
32
.github/workflows/playwright.yml
vendored
|
@ -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
|
||||
|
|
89
.github/workflows/windows.yml
vendored
89
.github/workflows/windows.yml
vendored
|
@ -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
|
|
@ -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",
|
||||
|
|
|
@ -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<CollectedMacro<'a>>) {
|
||||
for item in file.items.iter() {
|
||||
collect_from_item(item, macros);
|
||||
}
|
||||
MacroCollector::visit_file(&mut MacroCollector { macros }, file);
|
||||
}
|
||||
|
||||
pub fn collect_from_item<'a>(item: &'a Item, macros: &mut Vec<CollectedMacro<'a>>) {
|
||||
match item {
|
||||
Item::Fn(f) => collect_from_block(&f.block, macros),
|
||||
|
||||
// Ignore macros if they're not rsx or render
|
||||
Item::Macro(macro_) => {
|
||||
if macro_.mac.path.segments[0].ident == "rsx"
|
||||
|| macro_.mac.path.segments[0].ident == "render"
|
||||
{
|
||||
macros.push(¯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<CollectedMacro<'b>>,
|
||||
}
|
||||
|
||||
pub fn collect_from_block<'a>(block: &'a Block, macros: &mut Vec<CollectedMacro<'a>>) {
|
||||
for stmt in &block.stmts {
|
||||
match stmt {
|
||||
Stmt::Item(item) => collect_from_item(item, macros),
|
||||
Stmt::Local(local) => {
|
||||
if let Some(init) = &local.init {
|
||||
collect_from_expr(&init.expr, macros);
|
||||
}
|
||||
}
|
||||
Stmt::Expr(expr, _) => collect_from_expr(expr, macros),
|
||||
Stmt::Macro(mac) => {
|
||||
if mac.mac.path.segments[0].ident == "rsx"
|
||||
|| mac.mac.path.segments[0].ident == "render"
|
||||
{
|
||||
macros.push(&mac.mac);
|
||||
}
|
||||
}
|
||||
}
|
||||
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<CollectedMacro<'a>>) {
|
||||
// collect an expr from the exprs, descending into blocks
|
||||
match expr {
|
||||
Expr::Macro(macro_) => {
|
||||
if macro_.mac.path.segments[0].ident == "rsx"
|
||||
|| macro_.mac.path.segments[0].ident == "render"
|
||||
{
|
||||
macros.push(¯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 {
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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 } => {
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -58,7 +58,7 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
|
|||
|
||||
let mut writer = Writer::new(contents);
|
||||
|
||||
// Dont parse nested macros
|
||||
// Don't parse nested macros
|
||||
let mut end_span = LineColumn { column: 0, line: 0 };
|
||||
for item in macros {
|
||||
let macro_path = &item.path.segments[0].ident;
|
||||
|
@ -68,16 +68,11 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
|
|||
continue;
|
||||
}
|
||||
|
||||
// item.parse_body::<CallBody>();
|
||||
let body = item.parse_body::<CallBody>().unwrap();
|
||||
|
||||
let rsx_start = macro_path.span().start();
|
||||
|
||||
writer.out.indent = &writer.src[rsx_start.line - 1]
|
||||
.chars()
|
||||
.take_while(|c| *c == ' ')
|
||||
.count()
|
||||
/ 4;
|
||||
writer.out.indent = leading_whitespaces(writer.src[rsx_start.line - 1]) / 4;
|
||||
|
||||
write_body(&mut writer, &body);
|
||||
|
||||
|
@ -231,3 +226,14 @@ pub(crate) fn write_ifmt(input: &IfmtInput, writable: &mut impl Write) -> std::f
|
|||
let display = DisplayIfmt(input);
|
||||
write!(writable, "{}", display)
|
||||
}
|
||||
|
||||
pub fn leading_whitespaces(input: &str) -> usize {
|
||||
input
|
||||
.chars()
|
||||
.map_while(|c| match c {
|
||||
' ' => Some(1),
|
||||
'\t' => Some(4),
|
||||
_ => None,
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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<BuildResult> {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<()> {
|
||||
pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResult> {
|
||||
log::info!("🚅 Running build [Desktop] command...");
|
||||
|
||||
let t_start = std::time::Instant::now();
|
||||
let ignore_files = build_assets(config)?;
|
||||
|
||||
let mut cmd = Command::new("cargo");
|
||||
cmd.current_dir(&config.crate_dir)
|
||||
let mut cmd = subprocess::Exec::cmd("cargo")
|
||||
.cwd(&config.crate_dir)
|
||||
.arg("build")
|
||||
.stdout(std::process::Stdio::inherit())
|
||||
.stderr(std::process::Stdio::inherit());
|
||||
.arg("--message-format=json");
|
||||
|
||||
if config.release {
|
||||
cmd.arg("--release");
|
||||
cmd = cmd.arg("--release");
|
||||
}
|
||||
if config.verbose {
|
||||
cmd.arg("--verbose");
|
||||
cmd = cmd.arg("--verbose");
|
||||
}
|
||||
|
||||
if config.custom_profile.is_some() {
|
||||
let custom_profile = config.custom_profile.as_ref().unwrap();
|
||||
cmd.arg("--profile");
|
||||
cmd.arg(custom_profile);
|
||||
cmd = cmd.arg("--profile").arg(custom_profile);
|
||||
}
|
||||
|
||||
if config.features.is_some() {
|
||||
let features_str = config.features.as_ref().unwrap().join(" ");
|
||||
cmd.arg("--features");
|
||||
cmd.arg(features_str);
|
||||
cmd = cmd.arg("--features").arg(features_str);
|
||||
}
|
||||
|
||||
match &config.executable {
|
||||
let cmd = match &config.executable {
|
||||
crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
|
||||
crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
|
||||
crate::ExecutableType::Example(name) => cmd.arg("--example").arg(name),
|
||||
};
|
||||
|
||||
let output = cmd.output()?;
|
||||
let warning_messages = prettier_build(cmd)?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(Error::BuildFailed("Program build failed.".into()));
|
||||
}
|
||||
let release_type = match config.release {
|
||||
true => "release",
|
||||
false => "debug",
|
||||
};
|
||||
|
||||
if output.status.success() {
|
||||
let release_type = match config.release {
|
||||
true => "release",
|
||||
false => "debug",
|
||||
};
|
||||
|
||||
let file_name: String;
|
||||
let mut res_path = match &config.executable {
|
||||
crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
|
||||
file_name = name.clone();
|
||||
config.target_dir.join(release_type).join(name)
|
||||
}
|
||||
crate::ExecutableType::Example(name) => {
|
||||
file_name = name.clone();
|
||||
config
|
||||
.target_dir
|
||||
.join(release_type)
|
||||
.join("examples")
|
||||
.join(name)
|
||||
}
|
||||
};
|
||||
|
||||
let target_file = if cfg!(windows) {
|
||||
res_path.set_extension("exe");
|
||||
format!("{}.exe", &file_name)
|
||||
} else {
|
||||
file_name
|
||||
};
|
||||
|
||||
if !config.out_dir.is_dir() {
|
||||
create_dir_all(&config.out_dir)?;
|
||||
let file_name: String;
|
||||
let mut res_path = match &config.executable {
|
||||
crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
|
||||
file_name = name.clone();
|
||||
config.target_dir.join(release_type).join(name)
|
||||
}
|
||||
copy(res_path, &config.out_dir.join(target_file))?;
|
||||
crate::ExecutableType::Example(name) => {
|
||||
file_name = name.clone();
|
||||
config
|
||||
.target_dir
|
||||
.join(release_type)
|
||||
.join("examples")
|
||||
.join(name)
|
||||
}
|
||||
};
|
||||
|
||||
// this code will copy all public file to the output dir
|
||||
if config.asset_dir.is_dir() {
|
||||
let copy_options = fs_extra::dir::CopyOptions {
|
||||
overwrite: true,
|
||||
skip_exist: false,
|
||||
buffer_size: 64000,
|
||||
copy_inside: false,
|
||||
content_only: false,
|
||||
depth: 0,
|
||||
};
|
||||
let target_file = if cfg!(windows) {
|
||||
res_path.set_extension("exe");
|
||||
format!("{}.exe", &file_name)
|
||||
} else {
|
||||
file_name
|
||||
};
|
||||
|
||||
for entry in std::fs::read_dir(&config.asset_dir)? {
|
||||
let path = entry?.path();
|
||||
if path.is_file() {
|
||||
std::fs::copy(&path, &config.out_dir.join(path.file_name().unwrap()))?;
|
||||
} else {
|
||||
match fs_extra::dir::copy(&path, &config.out_dir, ©_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<Vec<Diagnostic>> {
|
||||
|
|
|
@ -109,11 +109,17 @@ async fn autoformat_project(check: bool) -> Result<()> {
|
|||
})
|
||||
.await;
|
||||
|
||||
if res.is_err() {
|
||||
eprintln!("error formatting file: {}", _path.display());
|
||||
match res {
|
||||
Err(err) => {
|
||||
eprintln!("error formatting file: {}\n{err}", _path.display());
|
||||
None
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
eprintln!("error formatting file: {}\n{err}", _path.display());
|
||||
None
|
||||
}
|
||||
Ok(Ok(res)) => Some(res),
|
||||
}
|
||||
|
||||
res
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
|
@ -122,7 +128,7 @@ async fn autoformat_project(check: bool) -> Result<()> {
|
|||
let files_formatted: usize = counts
|
||||
.into_iter()
|
||||
.map(|f| match f {
|
||||
Ok(Ok(res)) => res,
|
||||
Some(res) => res,
|
||||
_ => 0,
|
||||
})
|
||||
.sum();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
use clap::ValueEnum;
|
||||
use serde::Serialize;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Config options for the build system.
|
||||
|
@ -26,8 +29,8 @@ pub struct ConfigOptsBuild {
|
|||
pub profile: Option<String>,
|
||||
|
||||
/// Build platform: support Web & Desktop [default: "default_platform"]
|
||||
#[clap(long)]
|
||||
pub platform: Option<String>,
|
||||
#[clap(long, value_enum)]
|
||||
pub platform: Option<Platform>,
|
||||
|
||||
/// Space separated list of features to activate
|
||||
#[clap(long)]
|
||||
|
@ -69,8 +72,8 @@ pub struct ConfigOptsServe {
|
|||
pub profile: Option<String>,
|
||||
|
||||
/// Build platform: support Web & Desktop [default: "default_platform"]
|
||||
#[clap(long)]
|
||||
pub platform: Option<String>,
|
||||
#[clap(long, value_enum)]
|
||||
pub platform: Option<Platform>,
|
||||
|
||||
/// Build with hot reloading rsx [default: false]
|
||||
#[clap(long)]
|
||||
|
@ -88,6 +91,16 @@ pub struct ConfigOptsServe {
|
|||
pub features: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)]
|
||||
pub enum Platform {
|
||||
#[clap(name = "web")]
|
||||
#[serde(rename = "web")]
|
||||
Web,
|
||||
#[clap(name = "desktop")]
|
||||
#[serde(rename = "desktop")]
|
||||
Desktop,
|
||||
}
|
||||
|
||||
/// Ensure the given value for `--public-url` is formatted correctly.
|
||||
pub fn parse_public_url(val: &str) -> String {
|
||||
let prefix = if !val.starts_with('/') { "/" } else { "" };
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::error::Result;
|
||||
use crate::{cfg::Platform, error::Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
|
@ -73,7 +73,7 @@ impl Default for DioxusConfig {
|
|||
Self {
|
||||
application: ApplicationConfig {
|
||||
name: "dioxus".into(),
|
||||
default_platform: "web".to_string(),
|
||||
default_platform: Platform::Web,
|
||||
out_dir: Some(PathBuf::from("dist")),
|
||||
asset_dir: Some(PathBuf::from("public")),
|
||||
|
||||
|
@ -115,7 +115,7 @@ impl Default for DioxusConfig {
|
|||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ApplicationConfig {
|
||||
pub name: String,
|
||||
pub default_platform: String,
|
||||
pub default_platform: Platform,
|
||||
pub out_dir: Option<PathBuf>,
|
||||
pub asset_dir: Option<PathBuf>,
|
||||
|
||||
|
|
248
packages/cli/src/server/desktop/mod.rs
Normal file
248
packages/cli/src/server/desktop/mod.rs
Normal file
|
@ -0,0 +1,248 @@
|
|||
use crate::{
|
||||
server::{
|
||||
output::{print_console_info, PrettierOptions},
|
||||
setup_file_watcher, setup_file_watcher_hot_reload,
|
||||
},
|
||||
BuildResult, CrateConfig, Result,
|
||||
};
|
||||
|
||||
use dioxus_hot_reload::HotReloadMsg;
|
||||
use dioxus_html::HtmlCtx;
|
||||
use dioxus_rsx::hot_reload::*;
|
||||
use interprocess_docfix::local_socket::LocalSocketListener;
|
||||
use std::{
|
||||
process::{Child, Command},
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
};
|
||||
use tokio::sync::broadcast::{self};
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
use plugin::PluginManager;
|
||||
|
||||
pub async fn startup(config: CrateConfig) -> Result<()> {
|
||||
// ctrl-c shutdown checker
|
||||
let _crate_config = config.clone();
|
||||
let _ = ctrlc::set_handler(move || {
|
||||
#[cfg(feature = "plugin")]
|
||||
let _ = PluginManager::on_serve_shutdown(&_crate_config);
|
||||
std::process::exit(0);
|
||||
});
|
||||
|
||||
match config.hot_reload {
|
||||
true => serve_hot_reload(config).await?,
|
||||
false => serve_default(config).await?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start the server without hot reload
|
||||
pub async fn serve_default(config: CrateConfig) -> Result<()> {
|
||||
let (child, first_build_result) = start_desktop(&config)?;
|
||||
let currently_running_child: RwLock<Child> = RwLock::new(child);
|
||||
|
||||
log::info!("🚀 Starting development server...");
|
||||
|
||||
// We got to own watcher so that it exists for the duration of serve
|
||||
// Otherwise full reload won't work.
|
||||
let _watcher = setup_file_watcher(
|
||||
{
|
||||
let config = config.clone();
|
||||
|
||||
move || {
|
||||
let mut current_child = currently_running_child.write().unwrap();
|
||||
current_child.kill()?;
|
||||
let (child, result) = start_desktop(&config)?;
|
||||
*current_child = child;
|
||||
Ok(result)
|
||||
}
|
||||
},
|
||||
&config,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Print serve info
|
||||
print_console_info(
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: vec![],
|
||||
warnings: first_build_result.warnings,
|
||||
elapsed_time: first_build_result.elapsed_time,
|
||||
},
|
||||
None,
|
||||
);
|
||||
|
||||
std::future::pending::<()>().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start the server without hot reload
|
||||
|
||||
/// Start dx serve with hot reload
|
||||
pub async fn serve_hot_reload(config: CrateConfig) -> Result<()> {
|
||||
let (_, first_build_result) = start_desktop(&config)?;
|
||||
|
||||
println!("🚀 Starting development server...");
|
||||
|
||||
// Setup hot reload
|
||||
let FileMapBuildResult { map, errors } =
|
||||
FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
|
||||
|
||||
println!("🚀 Starting development server...");
|
||||
|
||||
for err in errors {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
|
||||
let file_map = Arc::new(Mutex::new(map));
|
||||
|
||||
let (hot_reload_tx, mut hot_reload_rx) = broadcast::channel(100);
|
||||
|
||||
// States
|
||||
// The open interprocess sockets
|
||||
let channels = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
// Setup file watcher
|
||||
// We got to own watcher so that it exists for the duration of serve
|
||||
// Otherwise hot reload won't work.
|
||||
let _watcher = setup_file_watcher_hot_reload(
|
||||
&config,
|
||||
hot_reload_tx,
|
||||
file_map.clone(),
|
||||
{
|
||||
let config = config.clone();
|
||||
|
||||
let channels = channels.clone();
|
||||
move || {
|
||||
for channel in &mut *channels.lock().unwrap() {
|
||||
send_msg(HotReloadMsg::Shutdown, channel);
|
||||
}
|
||||
Ok(start_desktop(&config)?.1)
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Print serve info
|
||||
print_console_info(
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: vec![],
|
||||
warnings: first_build_result.warnings,
|
||||
elapsed_time: first_build_result.elapsed_time,
|
||||
},
|
||||
None,
|
||||
);
|
||||
|
||||
clear_paths();
|
||||
|
||||
match LocalSocketListener::bind("@dioxusin") {
|
||||
Ok(local_socket_stream) => {
|
||||
let aborted = Arc::new(Mutex::new(false));
|
||||
|
||||
// listen for connections
|
||||
std::thread::spawn({
|
||||
let file_map = file_map.clone();
|
||||
let channels = channels.clone();
|
||||
let aborted = aborted.clone();
|
||||
let _ = local_socket_stream.set_nonblocking(true);
|
||||
move || {
|
||||
loop {
|
||||
if let Ok(mut connection) = local_socket_stream.accept() {
|
||||
// send any templates than have changed before the socket connected
|
||||
let templates: Vec<_> = {
|
||||
file_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.map
|
||||
.values()
|
||||
.filter_map(|(_, template_slot)| *template_slot)
|
||||
.collect()
|
||||
};
|
||||
for template in templates {
|
||||
if !send_msg(
|
||||
HotReloadMsg::UpdateTemplate(template),
|
||||
&mut connection,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
channels.lock().unwrap().push(connection);
|
||||
println!("Connected to hot reloading 🚀");
|
||||
}
|
||||
if *aborted.lock().unwrap() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
while let Ok(template) = hot_reload_rx.recv().await {
|
||||
let channels = &mut *channels.lock().unwrap();
|
||||
let mut i = 0;
|
||||
while i < channels.len() {
|
||||
let channel = &mut channels[i];
|
||||
if send_msg(HotReloadMsg::UpdateTemplate(template), channel) {
|
||||
i += 1;
|
||||
} else {
|
||||
channels.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => println!("failed to connect to hot reloading\n{error}"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_paths() {
|
||||
if cfg!(target_os = "macos") {
|
||||
// On unix, if you force quit the application, it can leave the file socket open
|
||||
// This will cause the local socket listener to fail to open
|
||||
// We check if the file socket is already open from an old session and then delete it
|
||||
let paths = ["./dioxusin", "./@dioxusin"];
|
||||
for path in paths {
|
||||
let path = std::path::PathBuf::from(path);
|
||||
if path.exists() {
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_msg(msg: HotReloadMsg, channel: &mut impl std::io::Write) -> bool {
|
||||
if let Ok(msg) = serde_json::to_string(&msg) {
|
||||
if channel.write_all(msg.as_bytes()).is_err() {
|
||||
return false;
|
||||
}
|
||||
if channel.write_all(&[b'\n']).is_err() {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> {
|
||||
// Run the desktop application
|
||||
let result = crate::builder::build_desktop(config, true)?;
|
||||
|
||||
match &config.executable {
|
||||
crate::ExecutableType::Binary(name)
|
||||
| crate::ExecutableType::Lib(name)
|
||||
| crate::ExecutableType::Example(name) => {
|
||||
let mut file = config.out_dir.join(name);
|
||||
if cfg!(windows) {
|
||||
file.set_extension("exe");
|
||||
}
|
||||
let child = Command::new(file.to_str().unwrap()).spawn()?;
|
||||
|
||||
Ok((child, result))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,449 +1,27 @@
|
|||
use crate::{builder, serve::Serve, BuildResult, CrateConfig, Result};
|
||||
use axum::{
|
||||
body::{Full, HttpBody},
|
||||
extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
|
||||
http::{
|
||||
header::{HeaderName, HeaderValue},
|
||||
Method, Response, StatusCode,
|
||||
},
|
||||
response::IntoResponse,
|
||||
routing::{get, get_service},
|
||||
Router,
|
||||
};
|
||||
use axum_server::tls_rustls::RustlsConfig;
|
||||
use crate::{BuildResult, CrateConfig, Result};
|
||||
|
||||
use cargo_metadata::diagnostic::Diagnostic;
|
||||
use dioxus_core::Template;
|
||||
use dioxus_html::HtmlCtx;
|
||||
use dioxus_rsx::hot_reload::*;
|
||||
use notify::{RecommendedWatcher, Watcher};
|
||||
use std::{
|
||||
net::UdpSocket,
|
||||
path::PathBuf,
|
||||
process::Command,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use tokio::sync::broadcast::{self, Sender};
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody};
|
||||
use tower_http::{
|
||||
cors::{Any, CorsLayer},
|
||||
ServiceBuilderExt,
|
||||
};
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
use plugin::PluginManager;
|
||||
|
||||
mod proxy;
|
||||
|
||||
mod hot_reload;
|
||||
use hot_reload::*;
|
||||
use tokio::sync::broadcast::Sender;
|
||||
|
||||
mod output;
|
||||
use output::*;
|
||||
|
||||
pub struct BuildManager {
|
||||
config: CrateConfig,
|
||||
reload_tx: broadcast::Sender<()>,
|
||||
}
|
||||
|
||||
impl BuildManager {
|
||||
fn rebuild(&self) -> Result<BuildResult> {
|
||||
log::info!("🪁 Rebuild project");
|
||||
let result = builder::build(&self.config, true)?;
|
||||
// change the websocket reload state to true;
|
||||
// the page will auto-reload.
|
||||
if self
|
||||
.config
|
||||
.dioxus_config
|
||||
.web
|
||||
.watcher
|
||||
.reload_html
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let _ = Serve::regen_dev_page(&self.config);
|
||||
}
|
||||
let _ = self.reload_tx.send(());
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
struct WsReloadState {
|
||||
update: broadcast::Sender<()>,
|
||||
}
|
||||
|
||||
pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Result<()> {
|
||||
// ctrl-c shutdown checker
|
||||
let _crate_config = config.clone();
|
||||
let _ = ctrlc::set_handler(move || {
|
||||
#[cfg(feature = "plugin")]
|
||||
let _ = PluginManager::on_serve_shutdown(&_crate_config);
|
||||
std::process::exit(0);
|
||||
});
|
||||
|
||||
let ip = get_ip().unwrap_or(String::from("0.0.0.0"));
|
||||
|
||||
match config.hot_reload {
|
||||
true => serve_hot_reload(ip, port, config, start_browser).await?,
|
||||
false => serve_default(ip, port, config, start_browser).await?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start the server without hot reload
|
||||
pub async fn serve_default(
|
||||
ip: String,
|
||||
port: u16,
|
||||
config: CrateConfig,
|
||||
start_browser: bool,
|
||||
) -> Result<()> {
|
||||
let first_build_result = crate::builder::build(&config, false)?;
|
||||
|
||||
log::info!("🚀 Starting development server...");
|
||||
|
||||
// WS Reload Watching
|
||||
let (reload_tx, _) = broadcast::channel(100);
|
||||
|
||||
// We got to own watcher so that it exists for the duration of serve
|
||||
// Otherwise full reload won't work.
|
||||
let _watcher = setup_file_watcher(&config, port, ip.clone(), reload_tx.clone()).await?;
|
||||
|
||||
let ws_reload_state = Arc::new(WsReloadState {
|
||||
update: reload_tx.clone(),
|
||||
});
|
||||
|
||||
// HTTPS
|
||||
// Before console info so it can stop if mkcert isn't installed or fails
|
||||
let rustls_config = get_rustls(&config).await?;
|
||||
|
||||
// Print serve info
|
||||
print_console_info(
|
||||
&ip,
|
||||
port,
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: vec![],
|
||||
warnings: first_build_result.warnings,
|
||||
elapsed_time: first_build_result.elapsed_time,
|
||||
},
|
||||
);
|
||||
|
||||
// Router
|
||||
let router = setup_router(config, ws_reload_state, None).await?;
|
||||
|
||||
// Start server
|
||||
start_server(port, router, start_browser, rustls_config).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start dx serve with hot reload
|
||||
pub async fn serve_hot_reload(
|
||||
ip: String,
|
||||
port: u16,
|
||||
config: CrateConfig,
|
||||
start_browser: bool,
|
||||
) -> Result<()> {
|
||||
let first_build_result = crate::builder::build(&config, false)?;
|
||||
|
||||
log::info!("🚀 Starting development server...");
|
||||
|
||||
// Setup hot reload
|
||||
let (reload_tx, _) = broadcast::channel(100);
|
||||
let FileMapBuildResult { map, errors } =
|
||||
FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
|
||||
|
||||
for err in errors {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
|
||||
let file_map = Arc::new(Mutex::new(map));
|
||||
let build_manager = Arc::new(BuildManager {
|
||||
config: config.clone(),
|
||||
reload_tx: reload_tx.clone(),
|
||||
});
|
||||
|
||||
let hot_reload_tx = broadcast::channel(100).0;
|
||||
|
||||
// States
|
||||
let hot_reload_state = Arc::new(HotReloadState {
|
||||
messages: hot_reload_tx.clone(),
|
||||
build_manager: build_manager.clone(),
|
||||
file_map: file_map.clone(),
|
||||
watcher_config: config.clone(),
|
||||
});
|
||||
|
||||
let ws_reload_state = Arc::new(WsReloadState {
|
||||
update: reload_tx.clone(),
|
||||
});
|
||||
|
||||
// Setup file watcher
|
||||
// We got to own watcher so that it exists for the duration of serve
|
||||
// Otherwise hot reload won't work.
|
||||
let _watcher = setup_file_watcher_hot_reload(
|
||||
&config,
|
||||
port,
|
||||
ip.clone(),
|
||||
hot_reload_tx,
|
||||
file_map,
|
||||
build_manager,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// HTTPS
|
||||
// Before console info so it can stop if mkcert isn't installed or fails
|
||||
let rustls_config = get_rustls(&config).await?;
|
||||
|
||||
// Print serve info
|
||||
print_console_info(
|
||||
&ip,
|
||||
port,
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: vec![],
|
||||
warnings: first_build_result.warnings,
|
||||
elapsed_time: first_build_result.elapsed_time,
|
||||
},
|
||||
);
|
||||
|
||||
// Router
|
||||
let router = setup_router(config, ws_reload_state, Some(hot_reload_state)).await?;
|
||||
|
||||
// Start server
|
||||
start_server(port, router, start_browser, rustls_config).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const DEFAULT_KEY_PATH: &str = "ssl/key.pem";
|
||||
const DEFAULT_CERT_PATH: &str = "ssl/cert.pem";
|
||||
|
||||
/// Returns an enum of rustls config and a bool if mkcert isn't installed
|
||||
async fn get_rustls(config: &CrateConfig) -> Result<Option<RustlsConfig>> {
|
||||
let web_config = &config.dioxus_config.web.https;
|
||||
if web_config.enabled != Some(true) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let (cert_path, key_path) = match web_config.mkcert {
|
||||
// mkcert, use it
|
||||
Some(true) => {
|
||||
// Get paths to store certs, otherwise use ssl/item.pem
|
||||
let key_path = web_config
|
||||
.key_path
|
||||
.clone()
|
||||
.unwrap_or(DEFAULT_KEY_PATH.to_string());
|
||||
|
||||
let cert_path = web_config
|
||||
.cert_path
|
||||
.clone()
|
||||
.unwrap_or(DEFAULT_CERT_PATH.to_string());
|
||||
|
||||
// Create ssl directory if using defaults
|
||||
if key_path == DEFAULT_KEY_PATH && cert_path == DEFAULT_CERT_PATH {
|
||||
_ = fs::create_dir("ssl");
|
||||
}
|
||||
|
||||
let cmd = Command::new("mkcert")
|
||||
.args([
|
||||
"-install",
|
||||
"-key-file",
|
||||
&key_path,
|
||||
"-cert-file",
|
||||
&cert_path,
|
||||
"localhost",
|
||||
"::1",
|
||||
"127.0.0.1",
|
||||
])
|
||||
.spawn();
|
||||
|
||||
match cmd {
|
||||
Err(e) => {
|
||||
match e.kind() {
|
||||
io::ErrorKind::NotFound => log::error!("mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions."),
|
||||
e => log::error!("an error occured while generating mkcert certificates: {}", e.to_string()),
|
||||
};
|
||||
return Err("failed to generate mkcert certificates".into());
|
||||
}
|
||||
Ok(mut cmd) => {
|
||||
cmd.wait()?;
|
||||
}
|
||||
}
|
||||
|
||||
(cert_path, key_path)
|
||||
}
|
||||
// not mkcert
|
||||
Some(false) => {
|
||||
// get paths to cert & key
|
||||
if let (Some(key), Some(cert)) =
|
||||
(web_config.key_path.clone(), web_config.cert_path.clone())
|
||||
{
|
||||
(cert, key)
|
||||
} else {
|
||||
// missing cert or key
|
||||
return Err("https is enabled but cert or key path is missing".into());
|
||||
}
|
||||
}
|
||||
// other
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
Ok(Some(
|
||||
RustlsConfig::from_pem_file(cert_path, key_path).await?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Sets up and returns a router
|
||||
async fn setup_router(
|
||||
config: CrateConfig,
|
||||
ws_reload: Arc<WsReloadState>,
|
||||
hot_reload: Option<Arc<HotReloadState>>,
|
||||
) -> Result<Router> {
|
||||
// Setup cors
|
||||
let cors = CorsLayer::new()
|
||||
// allow `GET` and `POST` when accessing the resource
|
||||
.allow_methods([Method::GET, Method::POST])
|
||||
// allow requests from any origin
|
||||
.allow_origin(Any)
|
||||
.allow_headers(Any);
|
||||
|
||||
let (coep, coop) = if config.cross_origin_policy {
|
||||
(
|
||||
HeaderValue::from_static("require-corp"),
|
||||
HeaderValue::from_static("same-origin"),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
HeaderValue::from_static("unsafe-none"),
|
||||
HeaderValue::from_static("unsafe-none"),
|
||||
)
|
||||
};
|
||||
|
||||
// Create file service
|
||||
let file_service_config = config.clone();
|
||||
let file_service = ServiceBuilder::new()
|
||||
.override_response_header(
|
||||
HeaderName::from_static("cross-origin-embedder-policy"),
|
||||
coep,
|
||||
)
|
||||
.override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
|
||||
.and_then(
|
||||
move |response: Response<ServeFileSystemResponseBody>| async move {
|
||||
let response = if file_service_config
|
||||
.dioxus_config
|
||||
.web
|
||||
.watcher
|
||||
.index_on_404
|
||||
.unwrap_or(false)
|
||||
&& response.status() == StatusCode::NOT_FOUND
|
||||
{
|
||||
let body = Full::from(
|
||||
// TODO: Cache/memoize this.
|
||||
std::fs::read_to_string(
|
||||
file_service_config
|
||||
.crate_dir
|
||||
.join(file_service_config.out_dir)
|
||||
.join("index.html"),
|
||||
)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
)
|
||||
.map_err(|err| match err {})
|
||||
.boxed();
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body(body)
|
||||
.unwrap()
|
||||
} else {
|
||||
response.map(|body| body.boxed())
|
||||
};
|
||||
Ok(response)
|
||||
},
|
||||
)
|
||||
.service(ServeDir::new(config.crate_dir.join(&config.out_dir)));
|
||||
|
||||
// Setup websocket
|
||||
let mut router = Router::new().route("/_dioxus/ws", get(ws_handler));
|
||||
|
||||
// Setup proxy
|
||||
for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() {
|
||||
router = proxy::add_proxy(router, &proxy_config)?;
|
||||
}
|
||||
|
||||
// Route file service
|
||||
router = router.fallback(get_service(file_service).handle_error(
|
||||
|error: std::io::Error| async move {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Unhandled internal error: {}", error),
|
||||
)
|
||||
},
|
||||
));
|
||||
|
||||
// Setup routes
|
||||
router = router
|
||||
.route("/_dioxus/hot_reload", get(hot_reload_handler))
|
||||
.layer(cors)
|
||||
.layer(Extension(ws_reload));
|
||||
|
||||
if let Some(hot_reload) = hot_reload {
|
||||
router = router.layer(Extension(hot_reload))
|
||||
}
|
||||
|
||||
Ok(router)
|
||||
}
|
||||
|
||||
/// Starts dx serve with no hot reload
|
||||
async fn start_server(
|
||||
port: u16,
|
||||
router: Router,
|
||||
start_browser: bool,
|
||||
rustls: Option<RustlsConfig>,
|
||||
) -> Result<()> {
|
||||
// If plugins, call on_serve_start event
|
||||
#[cfg(feature = "plugin")]
|
||||
PluginManager::on_serve_start(&config)?;
|
||||
|
||||
// Parse address
|
||||
let addr = format!("0.0.0.0:{}", port).parse().unwrap();
|
||||
|
||||
// Open the browser
|
||||
if start_browser {
|
||||
match rustls {
|
||||
Some(_) => _ = open::that(format!("https://{}", addr)),
|
||||
None => _ = open::that(format!("http://{}", addr)),
|
||||
}
|
||||
}
|
||||
|
||||
// Start the server with or without rustls
|
||||
match rustls {
|
||||
Some(rustls) => {
|
||||
axum_server::bind_rustls(addr, rustls)
|
||||
.serve(router.into_make_service())
|
||||
.await?
|
||||
}
|
||||
None => {
|
||||
axum::Server::bind(&addr)
|
||||
.serve(router.into_make_service())
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub mod desktop;
|
||||
pub mod web;
|
||||
|
||||
/// Sets up a file watcher
|
||||
async fn setup_file_watcher(
|
||||
async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
|
||||
build_with: F,
|
||||
config: &CrateConfig,
|
||||
port: u16,
|
||||
watcher_ip: String,
|
||||
reload_tx: Sender<()>,
|
||||
web_info: Option<WebServerInfo>,
|
||||
) -> Result<RecommendedWatcher> {
|
||||
let build_manager = BuildManager {
|
||||
config: config.clone(),
|
||||
reload_tx,
|
||||
};
|
||||
|
||||
let mut last_update_time = chrono::Local::now().timestamp();
|
||||
|
||||
// file watcher: check file change
|
||||
|
@ -460,20 +38,19 @@ async fn setup_file_watcher(
|
|||
let config = watcher_config.clone();
|
||||
if let Ok(e) = info {
|
||||
if chrono::Local::now().timestamp() > last_update_time {
|
||||
match build_manager.rebuild() {
|
||||
match build_with() {
|
||||
Ok(res) => {
|
||||
last_update_time = chrono::Local::now().timestamp();
|
||||
|
||||
#[allow(clippy::redundant_clone)]
|
||||
print_console_info(
|
||||
&watcher_ip,
|
||||
port,
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: e.paths.clone(),
|
||||
warnings: res.warnings,
|
||||
elapsed_time: res.elapsed_time,
|
||||
},
|
||||
web_info.clone(),
|
||||
);
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
|
@ -502,13 +79,12 @@ async fn setup_file_watcher(
|
|||
|
||||
// Todo: reduce duplication and merge with setup_file_watcher()
|
||||
/// Sets up a file watcher with hot reload
|
||||
async fn setup_file_watcher_hot_reload(
|
||||
async fn setup_file_watcher_hot_reload<F: Fn() -> Result<BuildResult> + Send + 'static>(
|
||||
config: &CrateConfig,
|
||||
port: u16,
|
||||
watcher_ip: String,
|
||||
hot_reload_tx: Sender<Template<'static>>,
|
||||
file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
|
||||
build_manager: Arc<BuildManager>,
|
||||
build_with: F,
|
||||
web_info: Option<WebServerInfo>,
|
||||
) -> Result<RecommendedWatcher> {
|
||||
// file watcher: check file change
|
||||
let allow_watch_path = config
|
||||
|
@ -533,17 +109,16 @@ async fn setup_file_watcher_hot_reload(
|
|||
for path in evt.paths.clone() {
|
||||
// if this is not a rust file, rebuild the whole project
|
||||
if path.extension().and_then(|p| p.to_str()) != Some("rs") {
|
||||
match build_manager.rebuild() {
|
||||
match build_with() {
|
||||
Ok(res) => {
|
||||
print_console_info(
|
||||
&watcher_ip,
|
||||
port,
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: evt.paths,
|
||||
warnings: res.warnings,
|
||||
elapsed_time: res.elapsed_time,
|
||||
},
|
||||
web_info.clone(),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
|
@ -560,17 +135,16 @@ async fn setup_file_watcher_hot_reload(
|
|||
messages.extend(msgs);
|
||||
}
|
||||
Ok(UpdateResult::NeedsRebuild) => {
|
||||
match build_manager.rebuild() {
|
||||
match build_with() {
|
||||
Ok(res) => {
|
||||
print_console_info(
|
||||
&watcher_ip,
|
||||
port,
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: evt.paths,
|
||||
warnings: res.warnings,
|
||||
elapsed_time: res.elapsed_time,
|
||||
},
|
||||
web_info.clone(),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
|
@ -606,50 +180,3 @@ async fn setup_file_watcher_hot_reload(
|
|||
|
||||
Ok(watcher)
|
||||
}
|
||||
|
||||
/// Get the network ip
|
||||
fn get_ip() -> Option<String> {
|
||||
let socket = match UdpSocket::bind("0.0.0.0:0") {
|
||||
Ok(s) => s,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
match socket.connect("8.8.8.8:80") {
|
||||
Ok(()) => (),
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
match socket.local_addr() {
|
||||
Ok(addr) => Some(addr.ip().to_string()),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle websockets
|
||||
async fn ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
_: Option<TypedHeader<headers::UserAgent>>,
|
||||
Extension(state): Extension<Arc<WsReloadState>>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(|mut socket| async move {
|
||||
let mut rx = state.update.subscribe();
|
||||
let reload_watcher = tokio::spawn(async move {
|
||||
loop {
|
||||
rx.recv().await.unwrap();
|
||||
// ignore the error
|
||||
if socket
|
||||
.send(Message::Text(String::from("reload")))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// flush the errors after recompling
|
||||
rx = rx.resubscribe();
|
||||
}
|
||||
});
|
||||
|
||||
reload_watcher.await.unwrap();
|
||||
})
|
||||
}
|
||||
|
|
|
@ -11,7 +11,17 @@ pub struct PrettierOptions {
|
|||
pub elapsed_time: u128,
|
||||
}
|
||||
|
||||
pub fn print_console_info(ip: &String, port: u16, config: &CrateConfig, options: PrettierOptions) {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WebServerInfo {
|
||||
pub ip: String,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
pub fn print_console_info(
|
||||
config: &CrateConfig,
|
||||
options: PrettierOptions,
|
||||
web_info: Option<WebServerInfo>,
|
||||
) {
|
||||
if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") {
|
||||
"cls"
|
||||
} else {
|
||||
|
@ -70,26 +80,28 @@ pub fn print_console_info(ip: &String, port: u16, config: &CrateConfig, options:
|
|||
);
|
||||
}
|
||||
|
||||
if config.dioxus_config.web.https.enabled == Some(true) {
|
||||
println!(
|
||||
"\t> Local : {}",
|
||||
format!("https://localhost:{}/", port).blue()
|
||||
);
|
||||
println!(
|
||||
"\t> Network : {}",
|
||||
format!("https://{}:{}/", ip, port).blue()
|
||||
);
|
||||
println!("\t> HTTPS : {}", "Enabled".to_string().green());
|
||||
} else {
|
||||
println!(
|
||||
"\t> Local : {}",
|
||||
format!("http://localhost:{}/", port).blue()
|
||||
);
|
||||
println!(
|
||||
"\t> Network : {}",
|
||||
format!("http://{}:{}/", ip, port).blue()
|
||||
);
|
||||
println!("\t> HTTPS : {}", "Disabled".to_string().red());
|
||||
if let Some(WebServerInfo { ip, port }) = web_info {
|
||||
if config.dioxus_config.web.https.enabled == Some(true) {
|
||||
println!(
|
||||
"\t> Local : {}",
|
||||
format!("https://localhost:{}/", port).blue()
|
||||
);
|
||||
println!(
|
||||
"\t> Network : {}",
|
||||
format!("https://{}:{}/", ip, port).blue()
|
||||
);
|
||||
println!("\t> HTTPS : {}", "Enabled".to_string().green());
|
||||
} else {
|
||||
println!(
|
||||
"\t> Local : {}",
|
||||
format!("http://localhost:{}/", port).blue()
|
||||
);
|
||||
println!(
|
||||
"\t> Network : {}",
|
||||
format!("http://{}:{}/", ip, port).blue()
|
||||
);
|
||||
println!("\t> HTTPS : {}", "Disabled".to_string().red());
|
||||
}
|
||||
}
|
||||
println!();
|
||||
println!("\t> Profile : {}", profile.green());
|
||||
|
|
|
@ -10,12 +10,10 @@ use dioxus_html::HtmlCtx;
|
|||
use dioxus_rsx::hot_reload::FileMap;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use super::BuildManager;
|
||||
use crate::CrateConfig;
|
||||
|
||||
pub struct HotReloadState {
|
||||
pub messages: broadcast::Sender<Template<'static>>,
|
||||
pub build_manager: Arc<BuildManager>,
|
||||
pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
|
||||
pub watcher_config: CrateConfig,
|
||||
}
|
490
packages/cli/src/server/web/mod.rs
Normal file
490
packages/cli/src/server/web/mod.rs
Normal file
|
@ -0,0 +1,490 @@
|
|||
use crate::{
|
||||
builder,
|
||||
serve::Serve,
|
||||
server::{
|
||||
output::{print_console_info, PrettierOptions, WebServerInfo},
|
||||
setup_file_watcher, setup_file_watcher_hot_reload,
|
||||
},
|
||||
BuildResult, CrateConfig, Result,
|
||||
};
|
||||
use axum::{
|
||||
body::{Full, HttpBody},
|
||||
extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
|
||||
http::{
|
||||
header::{HeaderName, HeaderValue},
|
||||
Method, Response, StatusCode,
|
||||
},
|
||||
response::IntoResponse,
|
||||
routing::{get, get_service},
|
||||
Router,
|
||||
};
|
||||
use axum_server::tls_rustls::RustlsConfig;
|
||||
|
||||
use dioxus_html::HtmlCtx;
|
||||
use dioxus_rsx::hot_reload::*;
|
||||
use std::{
|
||||
net::UdpSocket,
|
||||
process::Command,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use tokio::sync::broadcast::{self, Sender};
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody};
|
||||
use tower_http::{
|
||||
cors::{Any, CorsLayer},
|
||||
ServiceBuilderExt,
|
||||
};
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
use plugin::PluginManager;
|
||||
|
||||
mod proxy;
|
||||
|
||||
mod hot_reload;
|
||||
use hot_reload::*;
|
||||
|
||||
struct WsReloadState {
|
||||
update: broadcast::Sender<()>,
|
||||
}
|
||||
|
||||
pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Result<()> {
|
||||
// ctrl-c shutdown checker
|
||||
let _crate_config = config.clone();
|
||||
let _ = ctrlc::set_handler(move || {
|
||||
#[cfg(feature = "plugin")]
|
||||
let _ = PluginManager::on_serve_shutdown(&_crate_config);
|
||||
std::process::exit(0);
|
||||
});
|
||||
|
||||
let ip = get_ip().unwrap_or(String::from("0.0.0.0"));
|
||||
|
||||
match config.hot_reload {
|
||||
true => serve_hot_reload(ip, port, config, start_browser).await?,
|
||||
false => serve_default(ip, port, config, start_browser).await?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start the server without hot reload
|
||||
pub async fn serve_default(
|
||||
ip: String,
|
||||
port: u16,
|
||||
config: CrateConfig,
|
||||
start_browser: bool,
|
||||
) -> Result<()> {
|
||||
let first_build_result = crate::builder::build(&config, false)?;
|
||||
|
||||
log::info!("🚀 Starting development server...");
|
||||
|
||||
// WS Reload Watching
|
||||
let (reload_tx, _) = broadcast::channel(100);
|
||||
|
||||
// We got to own watcher so that it exists for the duration of serve
|
||||
// Otherwise full reload won't work.
|
||||
let _watcher = setup_file_watcher(
|
||||
{
|
||||
let config = config.clone();
|
||||
let reload_tx = reload_tx.clone();
|
||||
move || build(&config, &reload_tx)
|
||||
},
|
||||
&config,
|
||||
Some(WebServerInfo {
|
||||
ip: ip.clone(),
|
||||
port,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let ws_reload_state = Arc::new(WsReloadState {
|
||||
update: reload_tx.clone(),
|
||||
});
|
||||
|
||||
// HTTPS
|
||||
// Before console info so it can stop if mkcert isn't installed or fails
|
||||
let rustls_config = get_rustls(&config).await?;
|
||||
|
||||
// Print serve info
|
||||
print_console_info(
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: vec![],
|
||||
warnings: first_build_result.warnings,
|
||||
elapsed_time: first_build_result.elapsed_time,
|
||||
},
|
||||
Some(crate::server::output::WebServerInfo {
|
||||
ip: ip.clone(),
|
||||
port,
|
||||
}),
|
||||
);
|
||||
|
||||
// Router
|
||||
let router = setup_router(config, ws_reload_state, None).await?;
|
||||
|
||||
// Start server
|
||||
start_server(port, router, start_browser, rustls_config).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start dx serve with hot reload
|
||||
pub async fn serve_hot_reload(
|
||||
ip: String,
|
||||
port: u16,
|
||||
config: CrateConfig,
|
||||
start_browser: bool,
|
||||
) -> Result<()> {
|
||||
let first_build_result = crate::builder::build(&config, false)?;
|
||||
|
||||
log::info!("🚀 Starting development server...");
|
||||
|
||||
// Setup hot reload
|
||||
let (reload_tx, _) = broadcast::channel(100);
|
||||
let FileMapBuildResult { map, errors } =
|
||||
FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
|
||||
|
||||
for err in errors {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
|
||||
let file_map = Arc::new(Mutex::new(map));
|
||||
|
||||
let hot_reload_tx = broadcast::channel(100).0;
|
||||
|
||||
// States
|
||||
let hot_reload_state = Arc::new(HotReloadState {
|
||||
messages: hot_reload_tx.clone(),
|
||||
file_map: file_map.clone(),
|
||||
watcher_config: config.clone(),
|
||||
});
|
||||
|
||||
let ws_reload_state = Arc::new(WsReloadState {
|
||||
update: reload_tx.clone(),
|
||||
});
|
||||
|
||||
// Setup file watcher
|
||||
// We got to own watcher so that it exists for the duration of serve
|
||||
// Otherwise hot reload won't work.
|
||||
let _watcher = setup_file_watcher_hot_reload(
|
||||
&config,
|
||||
hot_reload_tx,
|
||||
file_map,
|
||||
{
|
||||
let config = config.clone();
|
||||
let reload_tx = reload_tx.clone();
|
||||
move || build(&config, &reload_tx)
|
||||
},
|
||||
Some(WebServerInfo {
|
||||
ip: ip.clone(),
|
||||
port,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// HTTPS
|
||||
// Before console info so it can stop if mkcert isn't installed or fails
|
||||
let rustls_config = get_rustls(&config).await?;
|
||||
|
||||
// Print serve info
|
||||
print_console_info(
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: vec![],
|
||||
warnings: first_build_result.warnings,
|
||||
elapsed_time: first_build_result.elapsed_time,
|
||||
},
|
||||
Some(WebServerInfo {
|
||||
ip: ip.clone(),
|
||||
port,
|
||||
}),
|
||||
);
|
||||
|
||||
// Router
|
||||
let router = setup_router(config, ws_reload_state, Some(hot_reload_state)).await?;
|
||||
|
||||
// Start server
|
||||
start_server(port, router, start_browser, rustls_config).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const DEFAULT_KEY_PATH: &str = "ssl/key.pem";
|
||||
const DEFAULT_CERT_PATH: &str = "ssl/cert.pem";
|
||||
|
||||
/// Returns an enum of rustls config and a bool if mkcert isn't installed
|
||||
async fn get_rustls(config: &CrateConfig) -> Result<Option<RustlsConfig>> {
|
||||
let web_config = &config.dioxus_config.web.https;
|
||||
if web_config.enabled != Some(true) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let (cert_path, key_path) = match web_config.mkcert {
|
||||
// mkcert, use it
|
||||
Some(true) => {
|
||||
// Get paths to store certs, otherwise use ssl/item.pem
|
||||
let key_path = web_config
|
||||
.key_path
|
||||
.clone()
|
||||
.unwrap_or(DEFAULT_KEY_PATH.to_string());
|
||||
|
||||
let cert_path = web_config
|
||||
.cert_path
|
||||
.clone()
|
||||
.unwrap_or(DEFAULT_CERT_PATH.to_string());
|
||||
|
||||
// Create ssl directory if using defaults
|
||||
if key_path == DEFAULT_KEY_PATH && cert_path == DEFAULT_CERT_PATH {
|
||||
_ = fs::create_dir("ssl");
|
||||
}
|
||||
|
||||
let cmd = Command::new("mkcert")
|
||||
.args([
|
||||
"-install",
|
||||
"-key-file",
|
||||
&key_path,
|
||||
"-cert-file",
|
||||
&cert_path,
|
||||
"localhost",
|
||||
"::1",
|
||||
"127.0.0.1",
|
||||
])
|
||||
.spawn();
|
||||
|
||||
match cmd {
|
||||
Err(e) => {
|
||||
match e.kind() {
|
||||
io::ErrorKind::NotFound => log::error!("mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions."),
|
||||
e => log::error!("an error occured while generating mkcert certificates: {}", e.to_string()),
|
||||
};
|
||||
return Err("failed to generate mkcert certificates".into());
|
||||
}
|
||||
Ok(mut cmd) => {
|
||||
cmd.wait()?;
|
||||
}
|
||||
}
|
||||
|
||||
(cert_path, key_path)
|
||||
}
|
||||
// not mkcert
|
||||
Some(false) => {
|
||||
// get paths to cert & key
|
||||
if let (Some(key), Some(cert)) =
|
||||
(web_config.key_path.clone(), web_config.cert_path.clone())
|
||||
{
|
||||
(cert, key)
|
||||
} else {
|
||||
// missing cert or key
|
||||
return Err("https is enabled but cert or key path is missing".into());
|
||||
}
|
||||
}
|
||||
// other
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
Ok(Some(
|
||||
RustlsConfig::from_pem_file(cert_path, key_path).await?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Sets up and returns a router
|
||||
async fn setup_router(
|
||||
config: CrateConfig,
|
||||
ws_reload: Arc<WsReloadState>,
|
||||
hot_reload: Option<Arc<HotReloadState>>,
|
||||
) -> Result<Router> {
|
||||
// Setup cors
|
||||
let cors = CorsLayer::new()
|
||||
// allow `GET` and `POST` when accessing the resource
|
||||
.allow_methods([Method::GET, Method::POST])
|
||||
// allow requests from any origin
|
||||
.allow_origin(Any)
|
||||
.allow_headers(Any);
|
||||
|
||||
let (coep, coop) = if config.cross_origin_policy {
|
||||
(
|
||||
HeaderValue::from_static("require-corp"),
|
||||
HeaderValue::from_static("same-origin"),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
HeaderValue::from_static("unsafe-none"),
|
||||
HeaderValue::from_static("unsafe-none"),
|
||||
)
|
||||
};
|
||||
|
||||
// Create file service
|
||||
let file_service_config = config.clone();
|
||||
let file_service = ServiceBuilder::new()
|
||||
.override_response_header(
|
||||
HeaderName::from_static("cross-origin-embedder-policy"),
|
||||
coep,
|
||||
)
|
||||
.override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
|
||||
.and_then(
|
||||
move |response: Response<ServeFileSystemResponseBody>| async move {
|
||||
let response = if file_service_config
|
||||
.dioxus_config
|
||||
.web
|
||||
.watcher
|
||||
.index_on_404
|
||||
.unwrap_or(false)
|
||||
&& response.status() == StatusCode::NOT_FOUND
|
||||
{
|
||||
let body = Full::from(
|
||||
// TODO: Cache/memoize this.
|
||||
std::fs::read_to_string(
|
||||
file_service_config
|
||||
.crate_dir
|
||||
.join(file_service_config.out_dir)
|
||||
.join("index.html"),
|
||||
)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
)
|
||||
.map_err(|err| match err {})
|
||||
.boxed();
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body(body)
|
||||
.unwrap()
|
||||
} else {
|
||||
response.map(|body| body.boxed())
|
||||
};
|
||||
Ok(response)
|
||||
},
|
||||
)
|
||||
.service(ServeDir::new(config.crate_dir.join(&config.out_dir)));
|
||||
|
||||
// Setup websocket
|
||||
let mut router = Router::new().route("/_dioxus/ws", get(ws_handler));
|
||||
|
||||
// Setup proxy
|
||||
for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() {
|
||||
router = proxy::add_proxy(router, &proxy_config)?;
|
||||
}
|
||||
|
||||
// Route file service
|
||||
router = router.fallback(get_service(file_service).handle_error(
|
||||
|error: std::io::Error| async move {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Unhandled internal error: {}", error),
|
||||
)
|
||||
},
|
||||
));
|
||||
|
||||
// Setup routes
|
||||
router = router
|
||||
.route("/_dioxus/hot_reload", get(hot_reload_handler))
|
||||
.layer(cors)
|
||||
.layer(Extension(ws_reload));
|
||||
|
||||
if let Some(hot_reload) = hot_reload {
|
||||
router = router.layer(Extension(hot_reload))
|
||||
}
|
||||
|
||||
Ok(router)
|
||||
}
|
||||
|
||||
/// Starts dx serve with no hot reload
|
||||
async fn start_server(
|
||||
port: u16,
|
||||
router: Router,
|
||||
start_browser: bool,
|
||||
rustls: Option<RustlsConfig>,
|
||||
) -> Result<()> {
|
||||
// If plugins, call on_serve_start event
|
||||
#[cfg(feature = "plugin")]
|
||||
PluginManager::on_serve_start(&config)?;
|
||||
|
||||
// Parse address
|
||||
let addr = format!("0.0.0.0:{}", port).parse().unwrap();
|
||||
|
||||
// Open the browser
|
||||
if start_browser {
|
||||
match rustls {
|
||||
Some(_) => _ = open::that(format!("https://{}", addr)),
|
||||
None => _ = open::that(format!("http://{}", addr)),
|
||||
}
|
||||
}
|
||||
|
||||
// Start the server with or without rustls
|
||||
match rustls {
|
||||
Some(rustls) => {
|
||||
axum_server::bind_rustls(addr, rustls)
|
||||
.serve(router.into_make_service())
|
||||
.await?
|
||||
}
|
||||
None => {
|
||||
axum::Server::bind(&addr)
|
||||
.serve(router.into_make_service())
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the network ip
|
||||
fn get_ip() -> Option<String> {
|
||||
let socket = match UdpSocket::bind("0.0.0.0:0") {
|
||||
Ok(s) => s,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
match socket.connect("8.8.8.8:80") {
|
||||
Ok(()) => (),
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
match socket.local_addr() {
|
||||
Ok(addr) => Some(addr.ip().to_string()),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle websockets
|
||||
async fn ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
_: Option<TypedHeader<headers::UserAgent>>,
|
||||
Extension(state): Extension<Arc<WsReloadState>>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(|mut socket| async move {
|
||||
let mut rx = state.update.subscribe();
|
||||
let reload_watcher = tokio::spawn(async move {
|
||||
loop {
|
||||
rx.recv().await.unwrap();
|
||||
// ignore the error
|
||||
if socket
|
||||
.send(Message::Text(String::from("reload")))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// flush the errors after recompling
|
||||
rx = rx.resubscribe();
|
||||
}
|
||||
});
|
||||
|
||||
reload_watcher.await.unwrap();
|
||||
})
|
||||
}
|
||||
|
||||
fn build(config: &CrateConfig, reload_tx: &Sender<()>) -> Result<BuildResult> {
|
||||
let result = builder::build(config, true)?;
|
||||
// change the websocket reload state to true;
|
||||
// the page will auto-reload.
|
||||
if config
|
||||
.dioxus_config
|
||||
.web
|
||||
.watcher
|
||||
.reload_html
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let _ = Serve::regen_dev_page(config);
|
||||
}
|
||||
let _ = reload_tx.send(());
|
||||
Ok(result)
|
||||
}
|
|
@ -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 }
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -15,7 +15,7 @@ use DynamicNode::*;
|
|||
|
||||
impl<'b> VirtualDom {
|
||||
pub(super) fn diff_scope(&mut self, scope: ScopeId) {
|
||||
let scope_state = &mut self.scopes[scope];
|
||||
let scope_state = &mut self.scopes[scope.0];
|
||||
|
||||
self.scope_stack.push(scope);
|
||||
unsafe {
|
||||
|
@ -129,12 +129,14 @@ impl<'b> VirtualDom {
|
|||
});
|
||||
|
||||
// Make sure the roots get transferred over while we're here
|
||||
right_template.root_ids.transfer(&left_template.root_ids);
|
||||
*right_template.root_ids.borrow_mut() = left_template.root_ids.borrow().clone();
|
||||
|
||||
let root_ids = right_template.root_ids.borrow();
|
||||
|
||||
// Update the node refs
|
||||
for i in 0..right_template.root_ids.len() {
|
||||
if let Some(root_id) = right_template.root_ids.get(i) {
|
||||
self.update_template(root_id, right_template);
|
||||
for i in 0..root_ids.len() {
|
||||
if let Some(root_id) = root_ids.get(i) {
|
||||
self.update_template(*root_id, right_template);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -189,7 +191,7 @@ impl<'b> VirtualDom {
|
|||
right.scope.set(Some(scope_id));
|
||||
|
||||
// copy out the box for both
|
||||
let old = self.scopes[scope_id].props.as_ref();
|
||||
let old = self.scopes[scope_id.0].props.as_ref();
|
||||
let new: Box<dyn AnyProps> = right.props.take().unwrap();
|
||||
let new: Box<dyn AnyProps> = unsafe { std::mem::transmute(new) };
|
||||
|
||||
|
@ -201,14 +203,14 @@ impl<'b> VirtualDom {
|
|||
}
|
||||
|
||||
// First, move over the props from the old to the new, dropping old props in the process
|
||||
self.scopes[scope_id].props = Some(new);
|
||||
self.scopes[scope_id.0].props = Some(new);
|
||||
|
||||
// Now run the component and diff it
|
||||
self.run_scope(scope_id);
|
||||
self.diff_scope(scope_id);
|
||||
|
||||
self.dirty_scopes.remove(&DirtyScope {
|
||||
height: self.scopes[scope_id].height,
|
||||
height: self.scopes[scope_id.0].height,
|
||||
id: scope_id,
|
||||
});
|
||||
}
|
||||
|
@ -686,7 +688,7 @@ impl<'b> VirtualDom {
|
|||
Some(node) => node,
|
||||
None => {
|
||||
self.mutations.push(Mutation::PushRoot {
|
||||
id: node.root_ids.get(idx).unwrap(),
|
||||
id: node.root_ids.borrow()[idx],
|
||||
});
|
||||
return 1;
|
||||
}
|
||||
|
@ -712,7 +714,7 @@ impl<'b> VirtualDom {
|
|||
|
||||
Component(comp) => {
|
||||
let scope = comp.scope.get().unwrap();
|
||||
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
|
||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
||||
RenderReturn::Ready(node) => self.push_all_real_nodes(node),
|
||||
RenderReturn::Aborted(_node) => todo!(),
|
||||
}
|
||||
|
@ -821,7 +823,7 @@ impl<'b> VirtualDom {
|
|||
if let Some(dy) = node.dynamic_root(idx) {
|
||||
self.remove_dynamic_node(dy, gen_muts);
|
||||
} else {
|
||||
let id = node.root_ids.get(idx).unwrap();
|
||||
let id = node.root_ids.borrow()[idx];
|
||||
if gen_muts {
|
||||
self.mutations.push(Mutation::Remove { id });
|
||||
}
|
||||
|
@ -913,13 +915,13 @@ impl<'b> VirtualDom {
|
|||
.expect("VComponents to always have a scope");
|
||||
|
||||
// Remove the component from the dom
|
||||
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
|
||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
||||
RenderReturn::Ready(t) => self.remove_node(t, gen_muts),
|
||||
RenderReturn::Aborted(placeholder) => self.remove_placeholder(placeholder, gen_muts),
|
||||
};
|
||||
|
||||
// Restore the props back to the vcomponent in case it gets rendered again
|
||||
let props = self.scopes[scope].props.take();
|
||||
let props = self.scopes[scope.0].props.take();
|
||||
*comp.props.borrow_mut() = unsafe { std::mem::transmute(props) };
|
||||
|
||||
// Now drop all the resouces
|
||||
|
@ -928,13 +930,13 @@ impl<'b> VirtualDom {
|
|||
|
||||
fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId {
|
||||
match node.dynamic_root(0) {
|
||||
None => node.root_ids.get(0).unwrap(),
|
||||
None => node.root_ids.borrow()[0],
|
||||
Some(Text(t)) => t.id.get().unwrap(),
|
||||
Some(Fragment(t)) => self.find_first_element(&t[0]),
|
||||
Some(Placeholder(t)) => t.id.get().unwrap(),
|
||||
Some(Component(comp)) => {
|
||||
let scope = comp.scope.get().unwrap();
|
||||
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
|
||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
||||
RenderReturn::Ready(t) => self.find_first_element(t),
|
||||
_ => todo!("cannot handle nonstandard nodes"),
|
||||
}
|
||||
|
@ -944,13 +946,13 @@ impl<'b> VirtualDom {
|
|||
|
||||
fn find_last_element(&self, node: &'b VNode<'b>) -> ElementId {
|
||||
match node.dynamic_root(node.template.get().roots.len() - 1) {
|
||||
None => node.root_ids.last().unwrap(),
|
||||
None => *node.root_ids.borrow().last().unwrap(),
|
||||
Some(Text(t)) => t.id.get().unwrap(),
|
||||
Some(Fragment(t)) => self.find_last_element(t.last().unwrap()),
|
||||
Some(Placeholder(t)) => t.id.get().unwrap(),
|
||||
Some(Component(comp)) => {
|
||||
let scope = comp.scope.get().unwrap();
|
||||
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
|
||||
match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
|
||||
RenderReturn::Ready(t) => self.find_last_element(t),
|
||||
_ => todo!("cannot handle nonstandard nodes"),
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ use bumpalo::boxed::Box as BumpBox;
|
|||
use bumpalo::Bump;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::{Cell, RefCell, UnsafeCell},
|
||||
cell::{Cell, RefCell},
|
||||
fmt::{Arguments, Debug},
|
||||
};
|
||||
|
||||
|
@ -54,7 +54,7 @@ pub struct VNode<'a> {
|
|||
|
||||
/// The IDs for the roots of this template - to be used when moving the template around and removing it from
|
||||
/// the actual Dom
|
||||
pub root_ids: BoxedCellSlice,
|
||||
pub root_ids: RefCell<Vec<ElementId>>,
|
||||
|
||||
/// The dynamic parts of the template
|
||||
pub dynamic_nodes: &'a [DynamicNode<'a>],
|
||||
|
@ -63,112 +63,13 @@ pub struct VNode<'a> {
|
|||
pub dynamic_attrs: &'a [Attribute<'a>],
|
||||
}
|
||||
|
||||
// Saftey: There is no way to get references to the internal data of this struct so no refrences will be invalidated by mutating the data with a immutable reference (The same principle behind Cell)
|
||||
#[derive(Debug, Default)]
|
||||
pub struct BoxedCellSlice(UnsafeCell<Option<Box<[ElementId]>>>);
|
||||
|
||||
impl Clone for BoxedCellSlice {
|
||||
fn clone(&self) -> Self {
|
||||
Self(UnsafeCell::new(unsafe { (*self.0.get()).clone() }))
|
||||
}
|
||||
}
|
||||
|
||||
impl BoxedCellSlice {
|
||||
pub fn last(&self) -> Option<ElementId> {
|
||||
unsafe {
|
||||
(*self.0.get())
|
||||
.as_ref()
|
||||
.and_then(|inner| inner.as_ref().last().copied())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, idx: usize) -> Option<ElementId> {
|
||||
unsafe {
|
||||
(*self.0.get())
|
||||
.as_ref()
|
||||
.and_then(|inner| inner.as_ref().get(idx).copied())
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn get_unchecked(&self, idx: usize) -> Option<ElementId> {
|
||||
(*self.0.get())
|
||||
.as_ref()
|
||||
.and_then(|inner| inner.as_ref().get(idx).copied())
|
||||
}
|
||||
|
||||
pub fn set(&self, idx: usize, new: ElementId) {
|
||||
unsafe {
|
||||
if let Some(inner) = &mut *self.0.get() {
|
||||
inner[idx] = new;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intialize(&self, contents: Box<[ElementId]>) {
|
||||
unsafe {
|
||||
*self.0.get() = Some(contents);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transfer(&self, other: &Self) {
|
||||
unsafe {
|
||||
*self.0.get() = (*other.0.get()).clone();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_from(&self, other: &Self) {
|
||||
unsafe {
|
||||
*self.0.get() = (*other.0.get()).take();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
unsafe {
|
||||
(*self.0.get())
|
||||
.as_ref()
|
||||
.map(|inner| inner.len())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a BoxedCellSlice {
|
||||
type Item = ElementId;
|
||||
|
||||
type IntoIter = BoxedCellSliceIter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
BoxedCellSliceIter {
|
||||
index: 0,
|
||||
borrow: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BoxedCellSliceIter<'a> {
|
||||
index: usize,
|
||||
borrow: &'a BoxedCellSlice,
|
||||
}
|
||||
|
||||
impl Iterator for BoxedCellSliceIter<'_> {
|
||||
type Item = ElementId;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let result = self.borrow.get(self.index);
|
||||
if result.is_some() {
|
||||
self.index += 1;
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> VNode<'a> {
|
||||
/// Create a template with no nodes that will be skipped over during diffing
|
||||
pub fn empty() -> Element<'a> {
|
||||
Some(VNode {
|
||||
key: None,
|
||||
parent: None,
|
||||
root_ids: BoxedCellSlice::default(),
|
||||
root_ids: Default::default(),
|
||||
dynamic_nodes: &[],
|
||||
dynamic_attrs: &[],
|
||||
template: Cell::new(Template {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -9,15 +9,12 @@ use crate::{
|
|||
AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId,
|
||||
};
|
||||
use bumpalo::{boxed::Box as BumpBox, Bump};
|
||||
use bumpslab::{BumpSlab, Slot};
|
||||
use rustc_hash::FxHashSet;
|
||||
use slab::{Slab, VacantEntry};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::{Cell, RefCell, UnsafeCell},
|
||||
fmt::{Arguments, Debug},
|
||||
future::Future,
|
||||
ops::{Index, IndexMut},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
@ -65,95 +62,6 @@ impl<'a, T> std::ops::Deref for Scoped<'a, T> {
|
|||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
|
||||
pub struct ScopeId(pub usize);
|
||||
|
||||
/// A thin wrapper around a BumpSlab that uses ids to index into the slab.
|
||||
pub(crate) struct ScopeSlab {
|
||||
slab: BumpSlab<ScopeState>,
|
||||
// a slab of slots of stable pointers to the ScopeState in the bump slab
|
||||
entries: Slab<Slot<'static, ScopeState>>,
|
||||
}
|
||||
|
||||
impl Drop for ScopeSlab {
|
||||
fn drop(&mut self) {
|
||||
// Bump slab doesn't drop its contents, so we need to do it manually
|
||||
for slot in self.entries.drain() {
|
||||
self.slab.remove(slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ScopeSlab {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
slab: BumpSlab::new(),
|
||||
entries: Slab::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ScopeSlab {
|
||||
pub(crate) fn get(&self, id: ScopeId) -> Option<&ScopeState> {
|
||||
self.entries.get(id.0).map(|slot| unsafe { &*slot.ptr() })
|
||||
}
|
||||
|
||||
pub(crate) fn get_mut(&mut self, id: ScopeId) -> Option<&mut ScopeState> {
|
||||
self.entries
|
||||
.get(id.0)
|
||||
.map(|slot| unsafe { &mut *slot.ptr_mut() })
|
||||
}
|
||||
|
||||
pub(crate) fn vacant_entry(&mut self) -> ScopeSlabEntry {
|
||||
let entry = self.entries.vacant_entry();
|
||||
ScopeSlabEntry {
|
||||
slab: &mut self.slab,
|
||||
entry,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&mut self, id: ScopeId) {
|
||||
self.slab.remove(self.entries.remove(id.0));
|
||||
}
|
||||
|
||||
pub(crate) fn contains(&self, id: ScopeId) -> bool {
|
||||
self.entries.contains(id.0)
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = &ScopeState> {
|
||||
self.entries.iter().map(|(_, slot)| unsafe { &*slot.ptr() })
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ScopeSlabEntry<'a> {
|
||||
slab: &'a mut BumpSlab<ScopeState>,
|
||||
entry: VacantEntry<'a, Slot<'static, ScopeState>>,
|
||||
}
|
||||
|
||||
impl<'a> ScopeSlabEntry<'a> {
|
||||
pub(crate) fn key(&self) -> ScopeId {
|
||||
ScopeId(self.entry.key())
|
||||
}
|
||||
|
||||
pub(crate) fn insert(self, scope: ScopeState) -> &'a ScopeState {
|
||||
let slot = self.slab.push(scope);
|
||||
// this is safe because the slot is only ever accessed with the lifetime of the borrow of the slab
|
||||
let slot = unsafe { std::mem::transmute(slot) };
|
||||
let entry = self.entry.insert(slot);
|
||||
unsafe { &*entry.ptr() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<ScopeId> for ScopeSlab {
|
||||
type Output = ScopeState;
|
||||
fn index(&self, id: ScopeId) -> &Self::Output {
|
||||
self.get(id).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexMut<ScopeId> for ScopeSlab {
|
||||
fn index_mut(&mut self, id: ScopeId) -> &mut Self::Output {
|
||||
self.get_mut(id).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// A component's state separate from its props.
|
||||
///
|
||||
/// This struct exists to provide a common interface for all scopes without relying on generics.
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
use crate::{
|
||||
any_props::VProps,
|
||||
arena::{ElementId, ElementRef},
|
||||
innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg, ScopeSlab},
|
||||
innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
|
||||
mutations::Mutation,
|
||||
nodes::RenderReturn,
|
||||
nodes::{Template, TemplateId},
|
||||
|
@ -176,7 +176,7 @@ use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
|
|||
pub struct VirtualDom {
|
||||
// Maps a template path to a map of byteindexes to templates
|
||||
pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template<'static>>>,
|
||||
pub(crate) scopes: ScopeSlab,
|
||||
pub(crate) scopes: Slab<Box<ScopeState>>,
|
||||
pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
|
||||
pub(crate) scheduler: Rc<Scheduler>,
|
||||
|
||||
|
@ -281,14 +281,14 @@ impl VirtualDom {
|
|||
///
|
||||
/// This is useful for inserting or removing contexts from a scope, or rendering out its root node
|
||||
pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
|
||||
self.scopes.get(id)
|
||||
self.scopes.get(id.0).map(|f| f.as_ref())
|
||||
}
|
||||
|
||||
/// Get the single scope at the top of the VirtualDom tree that will always be around
|
||||
///
|
||||
/// This scope has a ScopeId of 0 and is the root of the tree
|
||||
pub fn base_scope(&self) -> &ScopeState {
|
||||
self.scopes.get(ScopeId(0)).unwrap()
|
||||
self.get_scope(ScopeId(0)).unwrap()
|
||||
}
|
||||
|
||||
/// Build the virtualdom with a global context inserted into the base scope
|
||||
|
@ -303,7 +303,7 @@ impl VirtualDom {
|
|||
///
|
||||
/// Whenever the VirtualDom "works", it will re-render this scope
|
||||
pub fn mark_dirty(&mut self, id: ScopeId) {
|
||||
if let Some(scope) = self.scopes.get(id) {
|
||||
if let Some(scope) = self.get_scope(id) {
|
||||
let height = scope.height;
|
||||
self.dirty_scopes.insert(DirtyScope { height, id });
|
||||
}
|
||||
|
@ -496,7 +496,7 @@ impl VirtualDom {
|
|||
pub fn replace_template(&mut self, template: Template<'static>) {
|
||||
self.register_template_first_byte_index(template);
|
||||
// iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine
|
||||
for scope in self.scopes.iter() {
|
||||
for (_, scope) in self.scopes.iter() {
|
||||
if let Some(RenderReturn::Ready(sync)) = scope.try_root_node() {
|
||||
if sync.template.get().name.rsplit_once(':').unwrap().0
|
||||
== template.name.rsplit_once(':').unwrap().0
|
||||
|
@ -571,12 +571,15 @@ impl VirtualDom {
|
|||
/// The mutations will be thrown out, so it's best to use this method for things like SSR that have async content
|
||||
pub async fn wait_for_suspense(&mut self) {
|
||||
loop {
|
||||
// println!("waiting for suspense {:?}", self.suspended_scopes);
|
||||
if self.suspended_scopes.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// println!("waiting for suspense");
|
||||
self.wait_for_work().await;
|
||||
|
||||
// println!("Rendered immediately");
|
||||
_ = self.render_immediate();
|
||||
}
|
||||
}
|
||||
|
@ -598,7 +601,7 @@ impl VirtualDom {
|
|||
self.dirty_scopes.remove(&dirty);
|
||||
|
||||
// If the scope doesn't exist for whatever reason, then we should skip it
|
||||
if !self.scopes.contains(dirty.id) {
|
||||
if !self.scopes.contains(dirty.id.0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![cfg(not(miri))]
|
||||
|
||||
use dioxus::prelude::Props;
|
||||
use dioxus_core::*;
|
||||
use std::{cell::Cell, collections::HashSet};
|
||||
|
@ -314,6 +316,7 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
|
|||
}
|
||||
|
||||
// test for panics when creating random nodes and templates
|
||||
#[cfg(not(miri))]
|
||||
#[test]
|
||||
fn create() {
|
||||
for _ in 0..1000 {
|
||||
|
@ -325,6 +328,7 @@ fn create() {
|
|||
|
||||
// test for panics when diffing random nodes
|
||||
// This test will change the template every render which is not very realistic, but it helps stress the system
|
||||
#[cfg(not(miri))]
|
||||
#[test]
|
||||
fn diff() {
|
||||
for _ in 0..100000 {
|
||||
|
|
|
@ -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, "<div>Waiting for... child</div>");
|
||||
assert_eq!(out, "<div>Waiting for... child</div>");
|
||||
|
||||
dbg!(out);
|
||||
dbg!(out);
|
||||
});
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,42 +14,6 @@ pub(crate) struct FileDialogRequest {
|
|||
pub bubbles: bool,
|
||||
}
|
||||
|
||||
fn get_file_event_for_folder(request: &FileDialogRequest, dialog: rfd::FileDialog) -> Vec<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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<PathBuf> {
|
|||
target_os = "openbsd"
|
||||
))]
|
||||
pub(crate) fn get_file_event(request: &FileDialogRequest) -> Vec<PathBuf> {
|
||||
fn get_file_event_for_folder(
|
||||
request: &FileDialogRequest,
|
||||
dialog: rfd::FileDialog,
|
||||
) -> Vec<PathBuf> {
|
||||
if request.multiple {
|
||||
dialog.pick_folders().into_iter().flatten().collect()
|
||||
} else {
|
||||
dialog.pick_folder().into_iter().collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_file_event_for_file(
|
||||
request: &FileDialogRequest,
|
||||
mut dialog: rfd::FileDialog,
|
||||
) -> Vec<PathBuf> {
|
||||
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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
16
packages/extension/package-lock.json
generated
16
packages/extension/package-lock.json
generated
|
@ -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": {
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
#![allow(missing_docs)]
|
||||
use dioxus_core::{ScopeState, TaskId};
|
||||
use std::{
|
||||
any::Any,
|
||||
cell::{Cell, RefCell},
|
||||
future::{Future, IntoFuture},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{any::Any, cell::Cell, future::Future, rc::Rc, sync::Arc};
|
||||
|
||||
use crate::{use_state, UseState};
|
||||
|
||||
/// A future that resolves to a value.
|
||||
///
|
||||
|
@ -31,43 +27,30 @@ where
|
|||
F: Future<Output = T> + 'static,
|
||||
D: UseFutureDep,
|
||||
{
|
||||
let val = use_state(cx, || None);
|
||||
|
||||
let state = cx.use_hook(move || UseFuture {
|
||||
update: cx.schedule_update(),
|
||||
needs_regen: Cell::new(true),
|
||||
values: Default::default(),
|
||||
task: Cell::new(None),
|
||||
state: val.clone(),
|
||||
task: Default::default(),
|
||||
dependencies: Vec::new(),
|
||||
waker: Default::default(),
|
||||
});
|
||||
|
||||
*state.waker.borrow_mut() = None;
|
||||
|
||||
if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen.get() {
|
||||
// We don't need regen anymore
|
||||
state.needs_regen.set(false);
|
||||
// kill the old one, if it exists
|
||||
if let Some(task) = state.task.take() {
|
||||
cx.remove_future(task);
|
||||
}
|
||||
|
||||
// Create the new future
|
||||
let fut = future(dependencies.out());
|
||||
|
||||
// Clone in our cells
|
||||
let values = state.values.clone();
|
||||
let schedule_update = state.update.clone();
|
||||
let waker = state.waker.clone();
|
||||
|
||||
// Cancel the current future
|
||||
if let Some(current) = state.task.take() {
|
||||
cx.remove_future(current);
|
||||
}
|
||||
let val = val.clone();
|
||||
let task = state.task.clone();
|
||||
|
||||
state.task.set(Some(cx.push_future(async move {
|
||||
let res = fut.await;
|
||||
values.borrow_mut().push(Box::leak(Box::new(res)));
|
||||
|
||||
// if there's a waker, we dont re-render the component. Instead we just progress that future
|
||||
match waker.borrow().as_ref() {
|
||||
Some(waker) => waker.wake_by_ref(),
|
||||
None => schedule_update(),
|
||||
}
|
||||
val.set(Some(fut.await));
|
||||
task.take();
|
||||
})));
|
||||
}
|
||||
|
||||
|
@ -80,21 +63,12 @@ pub enum FutureState<'a, T> {
|
|||
Regenerating(&'a T), // the old value
|
||||
}
|
||||
|
||||
pub struct UseFuture<T> {
|
||||
pub struct UseFuture<T: 'static> {
|
||||
update: Arc<dyn Fn()>,
|
||||
needs_regen: Cell<bool>,
|
||||
task: Cell<Option<TaskId>>,
|
||||
task: Rc<Cell<Option<TaskId>>>,
|
||||
dependencies: Vec<Box<dyn Any>>,
|
||||
waker: Rc<RefCell<Option<std::task::Waker>>>,
|
||||
values: Rc<RefCell<Vec<*mut T>>>,
|
||||
}
|
||||
|
||||
impl<T> Drop for UseFuture<T> {
|
||||
fn drop(&mut self) {
|
||||
for value in self.values.take().into_iter() {
|
||||
drop(unsafe { Box::from_raw(value) })
|
||||
}
|
||||
}
|
||||
state: UseState<Option<T>>,
|
||||
}
|
||||
|
||||
pub enum UseFutureState<'a, T> {
|
||||
|
@ -120,30 +94,16 @@ impl<T> UseFuture<T> {
|
|||
}
|
||||
}
|
||||
|
||||
// clears the value in the future slot without starting the future over
|
||||
pub fn clear(&self) -> Option<T> {
|
||||
todo!()
|
||||
// (self.update)();
|
||||
// self.slot.replace(None)
|
||||
}
|
||||
|
||||
// Manually set the value in the future slot without starting the future over
|
||||
pub fn set(&self, _new_value: T) {
|
||||
// self.slot.set(Some(new_value));
|
||||
// self.needs_regen.set(true);
|
||||
// (self.update)();
|
||||
todo!()
|
||||
pub fn set(&self, new_value: T) {
|
||||
self.state.set(Some(new_value));
|
||||
}
|
||||
|
||||
/// Return any value, even old values if the future has not yet resolved.
|
||||
///
|
||||
/// If the future has never completed, the returned value will be `None`.
|
||||
pub fn value(&self) -> Option<&T> {
|
||||
self.values
|
||||
.borrow_mut()
|
||||
.last()
|
||||
.cloned()
|
||||
.map(|x| unsafe { &*x })
|
||||
self.state.current_val.as_ref().as_ref()
|
||||
}
|
||||
|
||||
/// Get the ID of the future in Dioxus' internal scheduler
|
||||
|
@ -169,35 +129,6 @@ impl<T> UseFuture<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, T> IntoFuture for &'a UseFuture<T> {
|
||||
type Output = &'a T;
|
||||
type IntoFuture = UseFutureAwait<'a, T>;
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
UseFutureAwait { hook: self }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UseFutureAwait<'a, T> {
|
||||
hook: &'a UseFuture<T>,
|
||||
}
|
||||
|
||||
impl<'a, T> Future for UseFutureAwait<'a, T> {
|
||||
type Output = &'a T;
|
||||
|
||||
fn poll(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Self::Output> {
|
||||
match self.hook.values.borrow_mut().last().cloned() {
|
||||
Some(value) => std::task::Poll::Ready(unsafe { &*value }),
|
||||
None => {
|
||||
self.hook.waker.replace(Some(cx.waker().clone()));
|
||||
std::task::Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait UseFutureDep: Sized + Clone {
|
||||
type Out;
|
||||
fn out(&self) -> Self::Out;
|
||||
|
@ -343,10 +274,6 @@ mod tests {
|
|||
let blah = "asd";
|
||||
});
|
||||
|
||||
let g2 = a.await;
|
||||
|
||||
let g = fut.await;
|
||||
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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()?,
|
||||
);
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,6 +12,6 @@
|
|||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.34.3"
|
||||
"@playwright/test": "^1.36.1"
|
||||
}
|
||||
}
|
62
playwright-tests/playwright-report/index.html
Normal file
62
playwright-tests/playwright-report/index.html
Normal file
File diff suppressed because one or more lines are too long
|
@ -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,
|
Loading…
Reference in a new issue