Merge branch 'master' into master
This commit is contained in:
313 files changed
+23988
-2334
No files matched your search
@@ -7,17 +7,16 @@ description = "Autofomatter for Dioxus RSX"
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
documentation = "https://dioxuslabs.com"
|
||||
keywords = ["dom", "ui", "gui", "react"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-rsx = { path = "../rsx", version = "^0.0.3" }
|
||||
dioxus-rsx = { workspace = true }
|
||||
proc-macro2 = { version = "1.0.6", features = ["span-locations"] }
|
||||
quote = "1.0"
|
||||
syn = { version = "1.0.11", features = ["full", "extra-traits"] }
|
||||
serde = { version = "1.0.136", features = ["derive"] }
|
||||
prettyplease = { package = "prettier-please", version = "0.1.16", features = [
|
||||
prettyplease = { package = "prettier-please", version = "0.1.16", features = [
|
||||
"verbatim",
|
||||
] }
|
||||
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# .github/workflows/build.yml
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: release ${{ matrix.target }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
archive: tar.gz tar.xz
|
||||
- target: x86_64-unknown-linux-musl
|
||||
archive: tar.gz tar.xz
|
||||
- target: x86_64-apple-darwin
|
||||
archive: tar.gz tar.xz
|
||||
- target: x86_64-pc-windows-gnu
|
||||
archive: zip
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Compile and release
|
||||
uses: rust-build/rust-build.action@latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RUSTTARGET: ${{ matrix.target }}
|
||||
ARCHIVE_TYPES: ${{ matrix.archive }}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
name: github pages
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- docs/**
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-20.04
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup mdBook
|
||||
uses: peaceiris/actions-mdbook@v1
|
||||
with:
|
||||
mdbook-version: '0.4.10'
|
||||
# mdbook-version: 'latest'
|
||||
|
||||
- run: cd docs && mdbook build
|
||||
|
||||
- name: Deploy 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@v4.2.3
|
||||
with:
|
||||
branch: gh-pages # The branch the action should deploy to.
|
||||
folder: docs/book # The folder the action should deploy.
|
||||
target-folder: docs/nightly/cli
|
||||
repository-name: dioxuslabs/docsite
|
||||
clean: false
|
||||
token: ${{ secrets.DEPLOY_KEY }} # let's pretend I don't need it for now
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
on: [push, pull_request]
|
||||
|
||||
name: Rust CI
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
|
||||
test:
|
||||
name: Test Suite
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
|
||||
fmt:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- run: rustup component add rustfmt
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
# clippy:
|
||||
# name: Clippy
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# - uses: actions-rs/toolchain@v1
|
||||
# with:
|
||||
# profile: minimal
|
||||
# toolchain: stable
|
||||
# override: true
|
||||
# - uses: Swatinem/rust-cache@v1
|
||||
# - run: rustup component add clippy
|
||||
# - uses: actions-rs/cargo@v1
|
||||
# with:
|
||||
# command: clippy
|
||||
# args: -- -D warnings
|
||||
@@ -0,0 +1,4 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
.DS_Store
|
||||
.idea/
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Lua.diagnostics.globals": [
|
||||
"plugin_logger",
|
||||
"PLUGIN_DOWNLOADER"
|
||||
]
|
||||
}
|
||||
Generated
+4778
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,98 @@
|
||||
[package]
|
||||
name = "dioxus-cli"
|
||||
version = "0.3.1"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2021"
|
||||
description = "CLI tool for developing, testing, and publishing Dioxus apps"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
license = "MIT/Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
# cli core
|
||||
clap = { version = "4.2", features = ["derive"] }
|
||||
thiserror = "1.0.30"
|
||||
wasm-bindgen-cli-support = "0.2"
|
||||
colored = "2.0.0"
|
||||
|
||||
# features
|
||||
log = "0.4.14"
|
||||
fern = { version = "0.6.0", features = ["colored"] }
|
||||
serde = { version = "1.0.136", features = ["derive"] }
|
||||
serde_json = "1.0.79"
|
||||
toml = "0.5.8"
|
||||
fs_extra = "1.2.0"
|
||||
cargo_toml = "0.11.4"
|
||||
futures = "0.3.21"
|
||||
notify = { version = "5.0.0-pre.16", features = ["serde"] }
|
||||
html_parser.workspace = true
|
||||
binary-install = "0.0.2"
|
||||
convert_case = "0.5.0"
|
||||
cargo_metadata = "0.15.0"
|
||||
tokio = { version = "1.16.1", features = ["full"] }
|
||||
atty = "0.2.14"
|
||||
regex = "1.5.4"
|
||||
chrono = "0.4.19"
|
||||
anyhow = "1.0.53"
|
||||
hyper = "0.14.17"
|
||||
hyper-rustls = "0.23.2"
|
||||
indicatif = "0.17.0-rc.11"
|
||||
subprocess = "0.2.9"
|
||||
|
||||
axum = { version = "0.5.1", features = ["ws", "headers"] }
|
||||
axum-server = { version = "0.5.1", features = ["tls-rustls"] }
|
||||
tower-http = { version = "0.2.2", features = ["full"] }
|
||||
headers = "0.3.7"
|
||||
|
||||
walkdir = "2"
|
||||
|
||||
# tools download
|
||||
dirs = "4.0.0"
|
||||
reqwest = { version = "0.11", features = [
|
||||
"rustls-tls",
|
||||
"stream",
|
||||
"trust-dns",
|
||||
"blocking",
|
||||
] }
|
||||
flate2 = "1.0.22"
|
||||
tar = "0.4.38"
|
||||
zip = "0.6.2"
|
||||
tower = "0.4.12"
|
||||
syn = { version = "1.0", features = ["full", "extra-traits"] }
|
||||
proc-macro2 = { version = "1.0", features = ["span-locations"] }
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
# plugin packages
|
||||
mlua = { version = "0.8.1", features = [
|
||||
"lua54",
|
||||
"vendored",
|
||||
"async",
|
||||
"send",
|
||||
"macros",
|
||||
], optional = true }
|
||||
ctrlc = "3.2.3"
|
||||
gitignore = "1.0.7"
|
||||
open = "4.1.0"
|
||||
cargo-generate = "0.18.3"
|
||||
toml_edit = "0.19.11"
|
||||
# dioxus-rsx = "0.0.1"
|
||||
|
||||
dioxus-autofmt = { workspace = true }
|
||||
rsx-rosetta = { workspace = true }
|
||||
dioxus-rsx = { workspace = true }
|
||||
dioxus-html = { workspace = true, features = ["hot-reload-context"] }
|
||||
dioxus-core = { workspace = true, features = ["serialize"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
plugin = ["mlua"]
|
||||
|
||||
# install path dx and dioxus as the same command
|
||||
# so, they're not really aliases
|
||||
# eventually dx will defer to the right version of dioxus
|
||||
[[bin]]
|
||||
path = "src/main.rs"
|
||||
name = "dioxus"
|
||||
|
||||
[[bin]]
|
||||
path = "src/main.rs"
|
||||
name = "dx"
|
||||
@@ -0,0 +1,45 @@
|
||||
[application]
|
||||
|
||||
# dioxus project name
|
||||
name = "dioxus-cli"
|
||||
|
||||
# default platfrom
|
||||
# you can also use `dioxus serve/build --platform XXX` to use other platform
|
||||
# value: web | desktop
|
||||
default_platform = "desktop"
|
||||
|
||||
# Web `build` & `serve` dist path
|
||||
out_dir = "dist"
|
||||
|
||||
# resource (static) file folder
|
||||
asset_dir = "public"
|
||||
|
||||
[web.app]
|
||||
|
||||
# HTML title tag content
|
||||
title = "dioxus | ⛺"
|
||||
|
||||
[web.watcher]
|
||||
|
||||
watch_path = ["src"]
|
||||
|
||||
# include `assets` in web platform
|
||||
[web.resource]
|
||||
|
||||
# CSS style file
|
||||
style = []
|
||||
|
||||
# Javascript code file
|
||||
script = []
|
||||
|
||||
[web.resource.dev]
|
||||
|
||||
# Javascript code file
|
||||
# serve: [dev-server] only
|
||||
script = []
|
||||
|
||||
[application.tools]
|
||||
|
||||
# use binaryen.wasm-opt for output Wasm file
|
||||
# binaryen just will trigger in `web` platform
|
||||
binaryen = { wasm_opt = true }
|
||||
@@ -0,0 +1,43 @@
|
||||
<div align="center">
|
||||
<h1>📦✨ Dioxus CLI </h1>
|
||||
<p><strong>Tooling to supercharge Dioxus projects</strong></p>
|
||||
</div>
|
||||
**dioxus-cli** (inspired by wasm-pack and webpack) is a tool for getting Dioxus projects up and running.
|
||||
It handles all build, bundling, development and publishing to simplify web development.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
### Install stable version
|
||||
```
|
||||
cargo install dioxus-cli
|
||||
```
|
||||
### Install from git repository
|
||||
```
|
||||
cargo install --git https://github.com/DioxusLabs/dioxus dioxus-cli
|
||||
```
|
||||
### Install from local folder
|
||||
```
|
||||
cargo install --path . --debug
|
||||
```
|
||||
|
||||
|
||||
## Get Started
|
||||
|
||||
Use `dioxus create project-name` to initialize a new Dioxus project. <br>
|
||||
|
||||
It will be cloned from the [dioxus-template](https://github.com/DioxusLabs/dioxus-template) repository.
|
||||
|
||||
<br>
|
||||
|
||||
Alternatively, you can specify the template path:
|
||||
|
||||
```
|
||||
dioxus create hello --template gh:dioxuslabs/dioxus-template
|
||||
```
|
||||
|
||||
## Dioxus Config File
|
||||
|
||||
Dioxus CLI will use `Dioxus.toml` file to Identify some project info and switch some cli feature.
|
||||
|
||||
You can get more configure information from [Dioxus CLI Document](https://dioxuslabs.com/cli/configure.html).
|
||||
@@ -0,0 +1,50 @@
|
||||
//! Construct version in the `commit-hash date channel` format
|
||||
|
||||
use std::{env, path::PathBuf, process::Command};
|
||||
|
||||
fn main() {
|
||||
set_rerun();
|
||||
set_commit_info();
|
||||
if option_env!("CFG_RELEASE").is_none() {
|
||||
println!("cargo:rustc-env=POKE_RA_DEVS=1");
|
||||
}
|
||||
}
|
||||
|
||||
fn set_rerun() {
|
||||
println!("cargo:rerun-if-env-changed=CFG_RELEASE");
|
||||
|
||||
let mut manifest_dir = PathBuf::from(
|
||||
env::var("CARGO_MANIFEST_DIR").expect("`CARGO_MANIFEST_DIR` is always set by cargo."),
|
||||
);
|
||||
|
||||
while manifest_dir.parent().is_some() {
|
||||
let head_ref = manifest_dir.join(".git/HEAD");
|
||||
if head_ref.exists() {
|
||||
println!("cargo:rerun-if-changed={}", head_ref.display());
|
||||
return;
|
||||
}
|
||||
|
||||
manifest_dir.pop();
|
||||
}
|
||||
|
||||
println!("cargo:warning=Could not find `.git/HEAD` from manifest dir!");
|
||||
}
|
||||
|
||||
fn set_commit_info() {
|
||||
let output = match Command::new("git")
|
||||
.arg("log")
|
||||
.arg("-1")
|
||||
.arg("--date=short")
|
||||
.arg("--format=%H %h %cd")
|
||||
.output()
|
||||
{
|
||||
Ok(output) if output.status.success() => output,
|
||||
_ => return,
|
||||
};
|
||||
let stdout = String::from_utf8(output.stdout).unwrap();
|
||||
let mut parts = stdout.split_whitespace();
|
||||
let mut next = || parts.next().unwrap();
|
||||
println!("cargo:rustc-env=RA_COMMIT_HASH={}", next());
|
||||
println!("cargo:rustc-env=RA_COMMIT_SHORT_HASH={}", next());
|
||||
println!("cargo:rustc-env=RA_COMMIT_DATE={}", next())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
book
|
||||
@@ -0,0 +1,6 @@
|
||||
[book]
|
||||
authors = ["YuKun Liu"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "Dioxus Cli"
|
||||
@@ -0,0 +1,18 @@
|
||||
# Summary
|
||||
|
||||
- [Introduction](./introduction.md)
|
||||
- [Installation](./installation.md)
|
||||
- [Create a Project](./creating.md)
|
||||
- [Configure Project](./configure.md)
|
||||
- [Commands](./cmd/README.md)
|
||||
- [Build](./cmd/build.md)
|
||||
- [Serve](./cmd/serve.md)
|
||||
- [Clean](./cmd/clean.md)
|
||||
- [Translate](./cmd/translate.md)
|
||||
- [Plugin Development](./plugin/README.md)
|
||||
- [API.Log](./plugin/interface/log.md)
|
||||
- [API.Command](./plugin/interface/command.md)
|
||||
- [API.OS](./plugin/interface/os.md)
|
||||
- [API.Directories](./plugin/interface/dirs.md)
|
||||
- [API.Network](./plugin/interface/network.md)
|
||||
- [API.Path](./plugin/interface/path.md)
|
||||
@@ -0,0 +1,26 @@
|
||||
# Commands
|
||||
|
||||
In this chapter we will introduce all `dioxus-cli` commands.
|
||||
|
||||
> You can also use `dioxus --help` to get cli help info.
|
||||
|
||||
```
|
||||
dioxus
|
||||
Build, bundle, & ship your Dioxus app
|
||||
|
||||
USAGE:
|
||||
dioxus [OPTIONS] <SUBCOMMAND>
|
||||
|
||||
OPTIONS:
|
||||
-h, --help Print help information
|
||||
-v Enable verbose logging
|
||||
|
||||
SUBCOMMANDS:
|
||||
build Build the Dioxus application and all of its assets
|
||||
clean Clean output artifacts
|
||||
config Dioxus config file controls
|
||||
create Init a new project for Dioxus
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
serve Build, watch & serve the Rust WASM app and all of its assets
|
||||
translate Translate some html file into a Dioxus component
|
||||
```
|
||||
@@ -0,0 +1,47 @@
|
||||
# Build
|
||||
|
||||
The `dioxus build` command can help you `pack & build` a dioxus project.
|
||||
|
||||
```
|
||||
dioxus-build
|
||||
Build the Rust WASM app and all of its assets
|
||||
|
||||
USAGE:
|
||||
dioxus build [OPTIONS]
|
||||
|
||||
OPTIONS:
|
||||
--example <EXAMPLE> [default: ""]
|
||||
--platform <PLATFORM> [default: "default_platform"]
|
||||
--release [default: false]
|
||||
```
|
||||
|
||||
You can use this command to build a project:
|
||||
|
||||
```
|
||||
dioxus build --release
|
||||
```
|
||||
|
||||
## Target platform
|
||||
|
||||
Use the `platform` option to choose your target platform:
|
||||
|
||||
```
|
||||
# for desktop project
|
||||
dioxus build --platform desktop
|
||||
```
|
||||
|
||||
`platform` currently only supports `desktop` & `web`.
|
||||
|
||||
```
|
||||
# for web project
|
||||
dioxus build --platform web
|
||||
```
|
||||
|
||||
## Build Example
|
||||
|
||||
You can use the `example` option to select a example to build:
|
||||
|
||||
```
|
||||
# build the `test` example
|
||||
dioxus build --exmaple test
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
# Clean
|
||||
|
||||
`dioxus clean` will clear the build artifacts (the out_dir and the cargo cache)
|
||||
|
||||
```
|
||||
dioxus-clean
|
||||
Clean build artifacts
|
||||
|
||||
USAGE:
|
||||
dioxus clean
|
||||
```
|
||||
|
||||
# Example
|
||||
|
||||
```
|
||||
dioxus clean
|
||||
```
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# Serve
|
||||
|
||||
The `dioxus serve` can start a dev server with hot-reloading
|
||||
|
||||
```
|
||||
dioxus-serve
|
||||
Build, watch & serve the Rust WASM app and all of its assets
|
||||
|
||||
USAGE:
|
||||
dioxus serve [OPTIONS]
|
||||
|
||||
OPTIONS:
|
||||
--example <EXAMPLE> [default: ""]
|
||||
--platform <PLATFORM> [default: "default_platform"]
|
||||
--release [default: false]
|
||||
--hot-reload [default: false]ß
|
||||
```
|
||||
|
||||
You can use this command to build project and start a dev server:
|
||||
|
||||
```
|
||||
dioxus serve
|
||||
```
|
||||
|
||||
## Serve Example
|
||||
|
||||
You can use the `example` option to serve a example:
|
||||
|
||||
```
|
||||
# serve the `test` example
|
||||
dioxus serve --exmaple test
|
||||
```
|
||||
|
||||
## Open Browser
|
||||
|
||||
You can add the `--open` option to open system default browser when server startup:
|
||||
|
||||
```
|
||||
dioxus serve --open
|
||||
```
|
||||
|
||||
## RSX Hot Reloading
|
||||
|
||||
You can add the `--hot-reload` flag to enable [rsx hot reloading](https://dioxuslabs.com/docs/0.3/guide/en/getting_started/hot_reload.html). This will allow you to reload some rsx changes without a full recompile:
|
||||
|
||||
```
|
||||
dioxus serve --open
|
||||
```
|
||||
|
||||
## Cross Origin Policy
|
||||
|
||||
You can add the `cross-origin-policy` option to change cross-origin header to:
|
||||
|
||||
```
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
Cross-Origin-Embedder-Policy: require-corp
|
||||
```
|
||||
|
||||
```
|
||||
dioxus serve --corss-origin-policy
|
||||
```
|
||||
@@ -0,0 +1,68 @@
|
||||
# Translate
|
||||
|
||||
`dioxus translate` can translate some `html` file into a Dioxus compoent
|
||||
|
||||
```
|
||||
dioxus-translate
|
||||
Translate some source file into a Dioxus component
|
||||
|
||||
USAGE:
|
||||
dioxus translate [OPTIONS] [OUTPUT]
|
||||
|
||||
ARGS:
|
||||
<OUTPUT> Output file, defaults to stdout if not present
|
||||
|
||||
OPTIONS:
|
||||
-c, --component Activate debug mode
|
||||
-f, --file <FILE> Input file
|
||||
```
|
||||
|
||||
## Translate HTML to stdout
|
||||
|
||||
You can use the `file` option to set path to the `html` file to translate:
|
||||
|
||||
```
|
||||
dioxus transtale --file ./index.html
|
||||
```
|
||||
|
||||
## Output rsx to a file
|
||||
|
||||
You can pass a file to the traslate command to set the path to write the output of the command to:
|
||||
|
||||
```
|
||||
dioxus translate --file ./index.html component.rsx
|
||||
```
|
||||
|
||||
## Output rsx to a file
|
||||
|
||||
Setting the `component` option will create a compoent from the HTML:
|
||||
|
||||
```
|
||||
dioxus translate --file ./index.html --component
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
This HTML:
|
||||
```html
|
||||
<div>
|
||||
<h1> Hello World </h1>
|
||||
<a href="https://dioxuslabs.com/">Link</a>
|
||||
</div>
|
||||
```
|
||||
|
||||
Translates into this Dioxus component:
|
||||
|
||||
```rust
|
||||
fn component(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
h1 { "Hello World" },
|
||||
a {
|
||||
href: "https://dioxuslabs.com/",
|
||||
"Link"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,208 @@
|
||||
# Configure Project
|
||||
|
||||
|
||||
This chapter will introduce you to how to configure the CLI with your `Dioxus.toml` file
|
||||
|
||||
Be aware that if the config file is present in the folder, some fields must be filled out, or the CLI tool will abort. The mandatory [table headers](https://toml.io/en/v1.0.0#table) and keys will have a '✍' sign beside it.
|
||||
|
||||
## Structure
|
||||
|
||||
The CLI uses a `Dioxus.toml` file in the root of your crate to define some configuration for your `dioxus` project.
|
||||
|
||||
### Application ✍
|
||||
|
||||
General application confiration:
|
||||
|
||||
```
|
||||
[application]
|
||||
# configuration
|
||||
```
|
||||
1. ***name*** ✍ - project name & title
|
||||
2. ***default_platform*** ✍ - which platform target for this project.
|
||||
|
||||
```
|
||||
name = "my-project"
|
||||
```
|
||||
2. ***default_platform*** - The platform this project targets
|
||||
```ß
|
||||
# current supported platforms: web, desktop
|
||||
# default: web
|
||||
default_platform = "web"
|
||||
```
|
||||
if you change this to `desktop`, the `dioxus build` will default building a desktop app
|
||||
3. ***out_dir*** - The directory to place the build artifacts from `dioxus build` or `dioxus service` into. This is also where the `assets` directory will be copied to
|
||||
```
|
||||
out_dir = "dist"
|
||||
```
|
||||
4. ***asset_dir*** - The directory with your static assets. The CLI will automatically copy these assets into the ***out_dir*** after a build/serve.
|
||||
```
|
||||
asset_dir = "public"
|
||||
```
|
||||
5. ***sub_package*** - The sub package in the workspace to build by default
|
||||
```
|
||||
sub_package = "my-crate"
|
||||
```
|
||||
|
||||
### Web.App ✍
|
||||
|
||||
Configeration specific to web applications:
|
||||
|
||||
```
|
||||
[web.app]
|
||||
# configuration
|
||||
```
|
||||
|
||||
1. ***title*** - The title of the web page
|
||||
```
|
||||
# HTML title tag content
|
||||
title = "dioxus app | ⛺"
|
||||
```
|
||||
2. ***base_path*** - The base path to build the appliation for serving at. This can be useful when serving your application in a subdirectory under a domain. For example when building a site to be served on github pages.
|
||||
```
|
||||
# The application will be served at domain.com/my_application/, so we need to modify the base_path to the path where the application will be served
|
||||
base_path = "my_application"
|
||||
```
|
||||
|
||||
### Web.Watcher ✍
|
||||
|
||||
Configeration related to the development server:
|
||||
|
||||
```
|
||||
[web.watcher]
|
||||
# configuration
|
||||
```
|
||||
|
||||
1. ***reload_html*** - If this is true, the cli will rebuild the index.html file every time the application is rebuilt
|
||||
```
|
||||
reload_html = true
|
||||
```
|
||||
2. ***watch_path*** - The files & directories to moniter for changes
|
||||
```
|
||||
watch_path = ["src", "public"]
|
||||
```
|
||||
3. ***index_on_404*** - If enabled, Dioxus CLI will serve the root page when a route is not found. *This is needed when serving an application that uses the router*
|
||||
```
|
||||
index_on_404 = true
|
||||
```
|
||||
|
||||
### Web.Resource ✍
|
||||
|
||||
Configeration related to static resources your application uses:
|
||||
```
|
||||
[web.resource]
|
||||
# configuration
|
||||
```
|
||||
|
||||
1. ***style*** - The styles (`.css` files) to include in your application
|
||||
```
|
||||
style = [
|
||||
# include from public_dir.
|
||||
"./assets/style.css",
|
||||
# or some asset from online cdn.
|
||||
"https://cdn.jsdelivr.net/npm/bootstrap/dist/css/bootstrap.css"
|
||||
]
|
||||
```
|
||||
2. ***script*** - The additional scripts (`.js` files) to include in your application
|
||||
```
|
||||
style = [
|
||||
# include from public_dir.
|
||||
"./assets/index.js",
|
||||
# or some asset from online cdn.
|
||||
"https://cdn.jsdelivr.net/npm/bootstrap/dist/js/bootstrap.js"
|
||||
]
|
||||
```
|
||||
|
||||
### Web.Resource.Dev ✍
|
||||
|
||||
Configeration related to static resources your application uses in development:
|
||||
```
|
||||
[web.resource.dev]
|
||||
# configuration
|
||||
```
|
||||
|
||||
1. ***style*** - The styles (`.css` files) to include in your application
|
||||
```
|
||||
style = [
|
||||
# include from public_dir.
|
||||
"./assets/style.css",
|
||||
# or some asset from online cdn.
|
||||
"https://cdn.jsdelivr.net/npm/bootstrap/dist/css/bootstrap.css"
|
||||
]
|
||||
```
|
||||
2. ***script*** - The additional scripts (`.js` files) to include in your application
|
||||
```
|
||||
style = [
|
||||
# include from public_dir.
|
||||
"./assets/index.js",
|
||||
# or some asset from online cdn.
|
||||
"https://cdn.jsdelivr.net/npm/bootstrap/dist/js/bootstrap.js"
|
||||
]
|
||||
```
|
||||
|
||||
### Web.Proxy
|
||||
|
||||
Configeration related to any proxies your application requires durring development. Proxies will forward requests to a new service
|
||||
|
||||
```
|
||||
[web.proxy]
|
||||
# configuration
|
||||
```
|
||||
|
||||
1. ***backend*** - The URL to the server to proxy. The CLI will forward any requests under the backend relative route to the backend instead of returning 404
|
||||
```
|
||||
backend = "http://localhost:8000/api/"
|
||||
```
|
||||
This will cause any requests made to the dev server with prefix /api/ to be redirected to the backend server at http://localhost:8000. The path and query parameters will be passed on as-is (path rewriting is not currently supported).
|
||||
|
||||
## Config example
|
||||
|
||||
```toml
|
||||
[application]
|
||||
|
||||
# App (Project) Name
|
||||
name = "{{project-name}}"
|
||||
|
||||
# The Dioxus platform to default to
|
||||
default_platform = "web"
|
||||
|
||||
# `build` & `serve` output path
|
||||
out_dir = "dist"
|
||||
|
||||
# the static resource path
|
||||
asset_dir = "public"
|
||||
|
||||
[web.app]
|
||||
|
||||
# HTML title tag content
|
||||
title = "dioxus | ⛺"
|
||||
|
||||
[web.watcher]
|
||||
|
||||
# when watcher is triggered, regenerate the `index.html`
|
||||
reload_html = true
|
||||
|
||||
# which files or dirs will be monitored
|
||||
watch_path = ["src", "public"]
|
||||
|
||||
# include `assets` in web platform
|
||||
[web.resource]
|
||||
|
||||
# CSS style file
|
||||
style = []
|
||||
|
||||
# Javascript code file
|
||||
script = []
|
||||
|
||||
[web.resource.dev]
|
||||
|
||||
# serve: [dev-server] only
|
||||
|
||||
# CSS style file
|
||||
style = []
|
||||
|
||||
# Javascript code file
|
||||
script = []
|
||||
|
||||
[[web.proxy]]
|
||||
backend = "http://localhost:8000/api/"
|
||||
```
|
||||
@@ -0,0 +1,39 @@
|
||||
# Create a Project
|
||||
|
||||
Once you have the Dioxus CLI tool installed, you can use it to create dioxus project.
|
||||
|
||||
## Initializing a default project
|
||||
|
||||
First, run the `dioxus create` command to create a new project ready to be used with Dioxus and the Dioxus CLI:
|
||||
|
||||
```
|
||||
dioxus create hello-dioxus
|
||||
```
|
||||
|
||||
> It will clone a default template from github template: [DioxusLabs/dioxus-template](https://github.com/DioxusLabs/dioxus-template)
|
||||
> This default template is use for `web` platform application.
|
||||
>
|
||||
> You can choose to create your project from a different template by passing the `template` argument:
|
||||
> ```
|
||||
> dioxus init hello-dioxus --template=gh:dioxuslabs/dioxus-template
|
||||
> ```
|
||||
|
||||
Next, move the current directory into your new project:
|
||||
|
||||
```
|
||||
cd hello-dioxus
|
||||
```
|
||||
|
||||
> Make sure `wasm32 target` is installed before running the Web project.
|
||||
> You can install the wasm target for rust using rustup:
|
||||
> ```
|
||||
> rustup target add wasm32-unknown-unknown
|
||||
> ```
|
||||
|
||||
Finally, create serve your project with the Dioxus CLI:
|
||||
|
||||
```
|
||||
dioxus serve
|
||||
```
|
||||
|
||||
By default, the CLI serve your site at: [`http://127.0.0.1:8080/`](http://127.0.0.1:8080/)
|
||||
@@ -0,0 +1,22 @@
|
||||
# Installation
|
||||
|
||||
Choose any one of the methods below to install the Dioxus CLI:
|
||||
|
||||
## Install from latest git version
|
||||
|
||||
To get the most up to date bug fixes and features of the Dioxus CLI, you can install the development version from git.
|
||||
|
||||
```
|
||||
cargo install --git https://github.com/Dioxuslabs/cli
|
||||
```
|
||||
|
||||
This will automatically download `Dioxus-CLI` source from github master branch,
|
||||
and install it in Cargo's global binary directory (`~/.cargo/bin/` by default).
|
||||
|
||||
## Install from `crates.io` version
|
||||
|
||||
The published version of the Dioxus CLI is updated less often, but should be more stable than the git version of the Dioxus CLI.
|
||||
|
||||
```
|
||||
cargo install dioxus-cli
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# Introduction
|
||||
|
||||
📦✨ **Dioxus-Cli** is a tool to help get dioxus projects off the ground.
|
||||
|
||||

|
||||
|
||||
It includes `dev server`, `hot reload` and some `quick command` to help you use dioxus.
|
||||
|
||||
## Features
|
||||
|
||||
- [x] `html` to `rsx` conversion tool
|
||||
- [x] hot reload for `web` platform
|
||||
- [x] create dioxus project from `git` repo
|
||||
- [x] build & pack dioxus project
|
||||
- [ ] autoformat dioxus `rsx` code
|
||||
|
||||
## Contributors
|
||||
|
||||
Contributors to this guide:
|
||||
|
||||
- [mrxiaozhuox](https://github.com/mrxiaozhuox)
|
||||
@@ -0,0 +1,79 @@
|
||||
# CLI Plugin Development
|
||||
|
||||
> For Cli 0.2.0 we will add `plugin-develop` support.
|
||||
|
||||
Before the 0.2.0 we use `dioxus tool` to use & install some plugin, but we think that is not good for extend cli program, some people want tailwind support, some people want sass support, we can't add all this thing in to the cli source code and we don't have time to maintain a lot of tools that user request, so maybe user make plugin by themself is a good choice.
|
||||
|
||||
### Why Lua ?
|
||||
|
||||
We choose `Lua: 5.4` to be the plugin develop language, because cli plugin is not complex, just like a workflow, and user & developer can write some easy code for their plugin. We have **vendored** lua in cli program, and user don't need install lua runtime in their computer, and the lua parser & runtime doesn't take up much disk memory.
|
||||
|
||||
### Event Management
|
||||
|
||||
The plugin library have pre-define some important event you can control:
|
||||
|
||||
- `build.on_start`
|
||||
- `build.on_finished`
|
||||
- `serve.on_start`
|
||||
- `serve.on_rebuild`
|
||||
- `serve.on_shutdown`
|
||||
|
||||
### Plugin Template
|
||||
|
||||
```lua
|
||||
package.path = library_dir .. "/?.lua"
|
||||
|
||||
local plugin = require("plugin")
|
||||
local manager = require("manager")
|
||||
|
||||
-- deconstruct api functions
|
||||
local log = plugin.log
|
||||
|
||||
-- plugin information
|
||||
manager.name = "Hello Dixous Plugin"
|
||||
manager.repository = "https://github.com/mrxiaozhuox/hello-dioxus-plugin"
|
||||
manager.author = "YuKun Liu <mrxzx.info@gmail.com>"
|
||||
manager.version = "0.0.1"
|
||||
|
||||
-- init manager info to plugin api
|
||||
plugin.init(manager)
|
||||
|
||||
manager.on_init = function ()
|
||||
-- when the first time plugin been load, this function will be execute.
|
||||
-- system will create a `dcp.json` file to verify init state.
|
||||
log.info("[plugin] Start to init plugin: " .. manager.name)
|
||||
end
|
||||
|
||||
---@param info BuildInfo
|
||||
manager.build.on_start = function (info)
|
||||
-- before the build work start, system will execute this function.
|
||||
log.info("[plugin] Build starting: " .. info.name)
|
||||
end
|
||||
|
||||
---@param info BuildInfo
|
||||
manager.build.on_finish = function (info)
|
||||
-- when the build work is done, system will execute this function.
|
||||
log.info("[plugin] Build finished: " .. info.name)
|
||||
end
|
||||
|
||||
---@param info ServeStartInfo
|
||||
manager.serve.on_start = function (info)
|
||||
-- this function will after clean & print to run, so you can print some thing.
|
||||
log.info("[plugin] Serve start: " .. info.name)
|
||||
end
|
||||
|
||||
---@param info ServeRebuildInfo
|
||||
manager.serve.on_rebuild = function (info)
|
||||
-- this function will after clean & print to run, so you can print some thing.
|
||||
local files = plugin.tool.dump(info.changed_files)
|
||||
log.info("[plugin] Serve rebuild: '" .. files .. "'")
|
||||
end
|
||||
|
||||
manager.serve.on_shutdown = function ()
|
||||
log.info("[plugin] Serve shutdown")
|
||||
end
|
||||
|
||||
manager.serve.interval = 1000
|
||||
|
||||
return manager
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
# Command Functions
|
||||
|
||||
> you can use command functions to execute some code & script
|
||||
|
||||
Type Define:
|
||||
```
|
||||
Stdio: "Inherit" | "Piped" | "Null"
|
||||
```
|
||||
|
||||
### `exec(commands: [string], stdout: Stdio, stderr: Stdio)`
|
||||
|
||||
you can use this function to run some command on the current system.
|
||||
|
||||
```lua
|
||||
local cmd = plugin.command
|
||||
|
||||
manager.test = function ()
|
||||
cmd.exec({"git", "clone", "https://github.com/DioxusLabs/cli-plugin-library"})
|
||||
end
|
||||
```
|
||||
> Warning: This function don't have exception catch.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Dirs Functions
|
||||
|
||||
> you can use Dirs functions to get some directory path
|
||||
|
||||
|
||||
### plugin_dir() -> string
|
||||
|
||||
You can get current plugin **root** directory path
|
||||
|
||||
```lua
|
||||
local path = plugin.dirs.plugin_dir()
|
||||
-- example: ~/Development/DioxusCli/plugin/test-plugin/
|
||||
```
|
||||
|
||||
### bin_dir() -> string
|
||||
|
||||
You can get plugin **bin** direcotry path
|
||||
|
||||
Sometime you need install some binary file like `tailwind-cli` & `sass-cli` to help your plugin work, then you should put binary file in this directory.
|
||||
|
||||
```lua
|
||||
local path = plugin.dirs.bin_dir()
|
||||
-- example: ~/Development/DioxusCli/plugin/test-plugin/bin/
|
||||
```
|
||||
|
||||
### temp_dir() -> string
|
||||
|
||||
You can get plugin **temp** direcotry path
|
||||
|
||||
Just put some temporary file in this directory.
|
||||
|
||||
```lua
|
||||
local path = plugin.dirs.bin_dir()
|
||||
-- example: ~/Development/DioxusCli/plugin/test-plugin/temp/
|
||||
```
|
||||
@@ -0,0 +1,48 @@
|
||||
# Log Functions
|
||||
|
||||
> You can use log function to print some useful log info
|
||||
|
||||
### Trace(info: string)
|
||||
|
||||
Print trace log info
|
||||
|
||||
```lua
|
||||
local log = plugin.log
|
||||
log.trace("trace information")
|
||||
```
|
||||
|
||||
### Debug(info: string)
|
||||
|
||||
Print debug log info
|
||||
|
||||
```lua
|
||||
local log = plugin.log
|
||||
log.debug("debug information")
|
||||
```
|
||||
|
||||
### Info(info: string)
|
||||
|
||||
Print info log info
|
||||
|
||||
```lua
|
||||
local log = plugin.log
|
||||
log.info("info information")
|
||||
```
|
||||
|
||||
### Warn(info: string)
|
||||
|
||||
Print warning log info
|
||||
|
||||
```lua
|
||||
local log = plugin.log
|
||||
log.warn("warn information")
|
||||
```
|
||||
|
||||
### Error(info: string)
|
||||
|
||||
Print error log info
|
||||
|
||||
```lua
|
||||
local log = plugin.log
|
||||
log.error("error information")
|
||||
```
|
||||
@@ -0,0 +1,34 @@
|
||||
# Network Functions
|
||||
|
||||
> you can use Network functions to download & read some data from internet
|
||||
|
||||
### download_file(url: string, path: string) -> boolean
|
||||
|
||||
This function can help you download some file from url, and it will return a *boolean* value to check the download status. (true: success | false: fail)
|
||||
|
||||
You need pass a target url and a local path (where you want to save this file)
|
||||
|
||||
```lua
|
||||
-- this file will download to plugin temp directory
|
||||
local status = plugin.network.download_file(
|
||||
"http://xxx.com/xxx.zip",
|
||||
plugin.dirs.temp_dir()
|
||||
)
|
||||
if status != true then
|
||||
log.error("Download Failed")
|
||||
end
|
||||
```
|
||||
|
||||
### clone_repo(url: string, path: string) -> boolean
|
||||
|
||||
This function can help you use `git clone` command (this system must have been installed git)
|
||||
|
||||
```lua
|
||||
local status = plugin.network.clone_repo(
|
||||
"http://github.com/mrxiaozhuox/dioxus-starter",
|
||||
plugin.dirs.bin_dir()
|
||||
)
|
||||
if status != true then
|
||||
log.error("Clone Failed")
|
||||
end
|
||||
```
|
||||
@@ -0,0 +1,11 @@
|
||||
# OS Functions
|
||||
|
||||
> you can use OS functions to get some system information
|
||||
|
||||
### current_platform() -> string ("windows" | "macos" | "linux")
|
||||
|
||||
This function can help you get system & platform type:
|
||||
|
||||
```lua
|
||||
local platform = plugin.os.current_platform()
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
# Path Functions
|
||||
|
||||
> you can use path functions to operate valid path string
|
||||
|
||||
### join(path: string, extra: string) -> string
|
||||
|
||||
This function can help you extend a path, you can extend any path, dirname or filename.
|
||||
|
||||
```lua
|
||||
local current_path = "~/hello/dioxus"
|
||||
local new_path = plugin.path.join(current_path, "world")
|
||||
-- new_path = "~/hello/dioxus/world"
|
||||
```
|
||||
|
||||
### parent(path: string) -> string
|
||||
|
||||
This function will return `path` parent-path string, back to the parent.
|
||||
|
||||
```lua
|
||||
local current_path = "~/hello/dioxus"
|
||||
local new_path = plugin.path.parent(current_path)
|
||||
-- new_path = "~/hello/"
|
||||
```
|
||||
|
||||
### exists(path: string) -> boolean
|
||||
|
||||
This function can check some path (dir & file) is exists.
|
||||
|
||||
### is_file(path: string) -> boolean
|
||||
|
||||
This function can check some path is a exist file.
|
||||
|
||||
### is_dir(path: string) -> boolean
|
||||
|
||||
This function can check some path is a exist dir.
|
||||
Whitespace-only changes.
@@ -0,0 +1,18 @@
|
||||
local Api = require("./interface")
|
||||
local log = Api.log;
|
||||
|
||||
local manager = {
|
||||
name = "Dioxus-CLI Plugin Demo",
|
||||
repository = "http://github.com/DioxusLabs/cli",
|
||||
author = "YuKun Liu <mrxzx.info@gmail.com>",
|
||||
}
|
||||
|
||||
manager.onLoad = function ()
|
||||
log.info("plugin loaded.")
|
||||
end
|
||||
|
||||
manager.onStartBuild = function ()
|
||||
log.warn("system start to build")
|
||||
end
|
||||
|
||||
return manager
|
||||
@@ -0,0 +1,25 @@
|
||||
local interface = {}
|
||||
|
||||
if plugin_logger ~= nil then
|
||||
interface.log = plugin_logger
|
||||
else
|
||||
interface.log = {
|
||||
trace = function (info)
|
||||
print("trace: " .. info)
|
||||
end,
|
||||
debug = function (info)
|
||||
print("debug: " .. info)
|
||||
end,
|
||||
info = function (info)
|
||||
print("info: " .. info)
|
||||
end,
|
||||
warn = function (info)
|
||||
print("warn: " .. info)
|
||||
end,
|
||||
error = function (info)
|
||||
print("error: " .. info)
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
return interface
|
||||
@@ -0,0 +1,25 @@
|
||||
// Dioxus-CLI
|
||||
// https://github.com/DioxusLabs/cli
|
||||
|
||||
(function () {
|
||||
var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
var url = protocol + '//' + window.location.host + '/_dioxus/ws';
|
||||
var poll_interval = 8080;
|
||||
var reload_upon_connect = () => {
|
||||
window.setTimeout(
|
||||
() => {
|
||||
var ws = new WebSocket(url);
|
||||
ws.onopen = () => window.location.reload();
|
||||
ws.onclose = reload_upon_connect;
|
||||
},
|
||||
poll_interval);
|
||||
};
|
||||
|
||||
var ws = new WebSocket(url);
|
||||
ws.onmessage = (ev) => {
|
||||
if (ev.data == "reload") {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
ws.onclose = reload_upon_connect;
|
||||
})()
|
||||
@@ -0,0 +1,47 @@
|
||||
[application]
|
||||
|
||||
# dioxus project name
|
||||
name = "{{project-name}}"
|
||||
|
||||
# default platfrom
|
||||
# you can also use `dioxus serve/build --platform XXX` to use other platform
|
||||
# value: web | desktop
|
||||
default_platform = "{{default-platform}}"
|
||||
|
||||
# Web `build` & `serve` dist path
|
||||
out_dir = "dist"
|
||||
|
||||
# resource (static) file folder
|
||||
asset_dir = "public"
|
||||
|
||||
[web.app]
|
||||
|
||||
# HTML title tag content
|
||||
title = "Dioxus | An elegant GUI library for Rust"
|
||||
|
||||
[web.watcher]
|
||||
|
||||
index_on_404 = true
|
||||
|
||||
watch_path = ["src"]
|
||||
|
||||
# include `assets` in web platform
|
||||
[web.resource]
|
||||
|
||||
# CSS style file
|
||||
style = []
|
||||
|
||||
# Javascript code file
|
||||
script = []
|
||||
|
||||
[web.resource.dev]
|
||||
|
||||
# Javascript code file
|
||||
# serve: [dev-server] only
|
||||
script = []
|
||||
|
||||
[application.plugins]
|
||||
|
||||
available = true
|
||||
|
||||
required = []
|
||||
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{app_title}</title>
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta charset="UTF-8" />
|
||||
{style_include}
|
||||
</head>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
<script type="module">
|
||||
import init from "/{base_path}/assets/dioxus/{app_name}.js";
|
||||
init("/{base_path}/assets/dioxus/{app_name}_bg.wasm").then(wasm => {
|
||||
if (wasm.__wbindgen_start == undefined) {
|
||||
wasm.main();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{script_include}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,725 @@
|
||||
use crate::{
|
||||
config::{CrateConfig, ExecutableType},
|
||||
error::{Error, Result},
|
||||
tools::Tool,
|
||||
DioxusConfig,
|
||||
};
|
||||
use cargo_metadata::{diagnostic::Diagnostic, Message};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use serde::Serialize;
|
||||
use std::{
|
||||
fs::{copy, create_dir_all, File},
|
||||
io::Read,
|
||||
panic,
|
||||
path::PathBuf,
|
||||
process::Command,
|
||||
time::Duration,
|
||||
};
|
||||
use wasm_bindgen_cli_support::Bindgen;
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct BuildResult {
|
||||
pub warnings: Vec<Diagnostic>,
|
||||
pub elapsed_time: u128,
|
||||
}
|
||||
|
||||
pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
|
||||
// [1] Build the project with cargo, generating a wasm32-unknown-unknown target (is there a more specific, better target to leverage?)
|
||||
// [2] Generate the appropriate build folders
|
||||
// [3] Wasm-bindgen the .wasm fiile, and move it into the {builddir}/modules/xxxx/xxxx_bg.wasm
|
||||
// [4] Wasm-opt the .wasm file with whatever optimizations need to be done
|
||||
// [5][OPTIONAL] Builds the Tailwind CSS file using the Tailwind standalone binary
|
||||
// [6] Link up the html page to the wasm module
|
||||
|
||||
let CrateConfig {
|
||||
out_dir,
|
||||
crate_dir,
|
||||
target_dir,
|
||||
asset_dir,
|
||||
executable,
|
||||
dioxus_config,
|
||||
..
|
||||
} = config;
|
||||
|
||||
// start to build the assets
|
||||
let ignore_files = build_assets(config)?;
|
||||
|
||||
let t_start = std::time::Instant::now();
|
||||
|
||||
// [1] Build the .wasm module
|
||||
log::info!("🚅 Running build command...");
|
||||
let cmd = subprocess::Exec::cmd("cargo");
|
||||
let cmd = cmd
|
||||
.cwd(crate_dir)
|
||||
.arg("build")
|
||||
.arg("--target")
|
||||
.arg("wasm32-unknown-unknown")
|
||||
.arg("--message-format=json");
|
||||
|
||||
let cmd = if config.release {
|
||||
cmd.arg("--release")
|
||||
} else {
|
||||
cmd
|
||||
};
|
||||
let cmd = if config.verbose {
|
||||
cmd.arg("--verbose")
|
||||
} else {
|
||||
cmd
|
||||
};
|
||||
|
||||
let cmd = if quiet { cmd.arg("--quiet") } else { cmd };
|
||||
|
||||
let cmd = if config.custom_profile.is_some() {
|
||||
let custom_profile = config.custom_profile.as_ref().unwrap();
|
||||
cmd.arg("--profile").arg(custom_profile)
|
||||
} else {
|
||||
cmd
|
||||
};
|
||||
|
||||
let cmd = if config.features.is_some() {
|
||||
let features_str = config.features.as_ref().unwrap().join(" ");
|
||||
cmd.arg("--features").arg(features_str)
|
||||
} else {
|
||||
cmd
|
||||
};
|
||||
|
||||
let cmd = match executable {
|
||||
ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
|
||||
ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
|
||||
ExecutableType::Example(name) => cmd.arg("--example").arg(name),
|
||||
};
|
||||
|
||||
let warning_messages = prettier_build(cmd)?;
|
||||
|
||||
// [2] Establish the output directory structure
|
||||
let bindgen_outdir = out_dir.join("assets").join("dioxus");
|
||||
|
||||
let release_type = match config.release {
|
||||
true => "release",
|
||||
false => "debug",
|
||||
};
|
||||
|
||||
let input_path = match executable {
|
||||
ExecutableType::Binary(name) | ExecutableType::Lib(name) => target_dir
|
||||
.join(format!("wasm32-unknown-unknown/{}", release_type))
|
||||
.join(format!("{}.wasm", name)),
|
||||
|
||||
ExecutableType::Example(name) => target_dir
|
||||
.join(format!("wasm32-unknown-unknown/{}/examples", release_type))
|
||||
.join(format!("{}.wasm", name)),
|
||||
};
|
||||
|
||||
let bindgen_result = panic::catch_unwind(move || {
|
||||
// [3] Bindgen the final binary for use easy linking
|
||||
let mut bindgen_builder = Bindgen::new();
|
||||
|
||||
bindgen_builder
|
||||
.input_path(input_path)
|
||||
.web(true)
|
||||
.unwrap()
|
||||
.debug(true)
|
||||
.demangle(true)
|
||||
.keep_debug(true)
|
||||
.remove_name_section(false)
|
||||
.remove_producers_section(false)
|
||||
.out_name(&dioxus_config.application.name)
|
||||
.generate(&bindgen_outdir)
|
||||
.unwrap();
|
||||
});
|
||||
if bindgen_result.is_err() {
|
||||
return Err(Error::BuildFailed("Bindgen build failed! \nThis is probably due to the Bindgen version, dioxus-cli using `0.2.81` Bindgen crate.".to_string()));
|
||||
}
|
||||
|
||||
// check binaryen:wasm-opt tool
|
||||
let dioxus_tools = dioxus_config.application.tools.clone().unwrap_or_default();
|
||||
if dioxus_tools.contains_key("binaryen") {
|
||||
let info = dioxus_tools.get("binaryen").unwrap();
|
||||
let binaryen = crate::tools::Tool::Binaryen;
|
||||
|
||||
if binaryen.is_installed() {
|
||||
if let Some(sub) = info.as_table() {
|
||||
if sub.contains_key("wasm_opt")
|
||||
&& sub.get("wasm_opt").unwrap().as_bool().unwrap_or(false)
|
||||
{
|
||||
log::info!("Optimizing WASM size with wasm-opt...");
|
||||
let target_file = out_dir
|
||||
.join("assets")
|
||||
.join("dioxus")
|
||||
.join(format!("{}_bg.wasm", dioxus_config.application.name));
|
||||
if target_file.is_file() {
|
||||
let mut args = vec![
|
||||
target_file.to_str().unwrap(),
|
||||
"-o",
|
||||
target_file.to_str().unwrap(),
|
||||
];
|
||||
if config.release {
|
||||
args.push("-Oz");
|
||||
}
|
||||
binaryen.call("wasm-opt", args)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::warn!(
|
||||
"Binaryen tool not found, you can use `dioxus tool add binaryen` to install it."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// [5][OPTIONAL] If tailwind is enabled and installed we run it to generate the CSS
|
||||
if dioxus_tools.contains_key("tailwindcss") {
|
||||
let info = dioxus_tools.get("tailwindcss").unwrap();
|
||||
let tailwind = crate::tools::Tool::Tailwind;
|
||||
|
||||
if tailwind.is_installed() {
|
||||
if let Some(sub) = info.as_table() {
|
||||
log::info!("Building Tailwind bundle CSS file...");
|
||||
|
||||
let input_path = match sub.get("input") {
|
||||
Some(val) => val.as_str().unwrap(),
|
||||
None => "./public",
|
||||
};
|
||||
let config_path = match sub.get("config") {
|
||||
Some(val) => val.as_str().unwrap(),
|
||||
None => "./src/tailwind.config.js",
|
||||
};
|
||||
let mut args = vec![
|
||||
"-i",
|
||||
input_path,
|
||||
"-o",
|
||||
"dist/tailwind.css",
|
||||
"-c",
|
||||
config_path,
|
||||
];
|
||||
|
||||
if config.release {
|
||||
args.push("--minify");
|
||||
}
|
||||
|
||||
tailwind.call("tailwindcss", args)?;
|
||||
}
|
||||
} else {
|
||||
log::warn!(
|
||||
"Tailwind tool not found, you can use `dioxus tool add tailwindcss` to install it."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// this code will copy all public file to the output dir
|
||||
let copy_options = fs_extra::dir::CopyOptions {
|
||||
overwrite: true,
|
||||
skip_exist: false,
|
||||
buffer_size: 64000,
|
||||
copy_inside: false,
|
||||
content_only: false,
|
||||
depth: 0,
|
||||
};
|
||||
if asset_dir.is_dir() {
|
||||
for entry in std::fs::read_dir(asset_dir)? {
|
||||
let path = entry?.path();
|
||||
if path.is_file() {
|
||||
std::fs::copy(&path, out_dir.join(path.file_name().unwrap()))?;
|
||||
} else {
|
||||
match fs_extra::dir::copy(&path, 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)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let t_end = std::time::Instant::now();
|
||||
Ok(BuildResult {
|
||||
warnings: warning_messages,
|
||||
elapsed_time: (t_end - t_start).as_millis(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<()> {
|
||||
log::info!("🚅 Running build [Desktop] command...");
|
||||
|
||||
let ignore_files = build_assets(config)?;
|
||||
|
||||
let mut cmd = Command::new("cargo");
|
||||
cmd.current_dir(&config.crate_dir)
|
||||
.arg("build")
|
||||
.stdout(std::process::Stdio::inherit())
|
||||
.stderr(std::process::Stdio::inherit());
|
||||
|
||||
if config.release {
|
||||
cmd.arg("--release");
|
||||
}
|
||||
if config.verbose {
|
||||
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);
|
||||
}
|
||||
|
||||
if config.features.is_some() {
|
||||
let features_str = config.features.as_ref().unwrap().join(" ");
|
||||
cmd.arg("--features");
|
||||
cmd.arg(features_str);
|
||||
}
|
||||
|
||||
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()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(Error::BuildFailed("Program build failed.".into()));
|
||||
}
|
||||
|
||||
if output.status.success() {
|
||||
let release_type = match config.release {
|
||||
true => "release",
|
||||
false => "debug",
|
||||
};
|
||||
|
||||
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)?;
|
||||
}
|
||||
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)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"🚩 Build completed: [./{}]",
|
||||
config
|
||||
.dioxus_config
|
||||
.application
|
||||
.out_dir
|
||||
.clone()
|
||||
.unwrap_or_else(|| PathBuf::from("dist"))
|
||||
.display()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result<Vec<Diagnostic>> {
|
||||
let mut warning_messages: Vec<Diagnostic> = vec![];
|
||||
|
||||
let pb = ProgressBar::new_spinner();
|
||||
pb.enable_steady_tick(Duration::from_millis(200));
|
||||
pb.set_style(
|
||||
ProgressStyle::with_template("{spinner:.dim.bold} {wide_msg}")
|
||||
.unwrap()
|
||||
.tick_chars("/|\\- "),
|
||||
);
|
||||
pb.set_message("💼 Waiting to start build the project...");
|
||||
|
||||
struct StopSpinOnDrop(ProgressBar);
|
||||
|
||||
impl Drop for StopSpinOnDrop {
|
||||
fn drop(&mut self) {
|
||||
self.0.finish_and_clear();
|
||||
}
|
||||
}
|
||||
|
||||
StopSpinOnDrop(pb.clone());
|
||||
|
||||
let stdout = cmd.detached().stream_stdout()?;
|
||||
let reader = std::io::BufReader::new(stdout);
|
||||
for message in cargo_metadata::Message::parse_stream(reader) {
|
||||
match message.unwrap() {
|
||||
Message::CompilerMessage(msg) => {
|
||||
let message = msg.message;
|
||||
match message.level {
|
||||
cargo_metadata::diagnostic::DiagnosticLevel::Error => {
|
||||
return {
|
||||
Err(anyhow::anyhow!(message
|
||||
.rendered
|
||||
.unwrap_or("Unknown".into())))
|
||||
};
|
||||
}
|
||||
cargo_metadata::diagnostic::DiagnosticLevel::Warning => {
|
||||
warning_messages.push(message.clone());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Message::CompilerArtifact(artifact) => {
|
||||
pb.set_message(format!("Compiling {} ", artifact.package_id));
|
||||
pb.tick();
|
||||
}
|
||||
Message::BuildScriptExecuted(script) => {
|
||||
let _package_id = script.package_id.to_string();
|
||||
}
|
||||
Message::BuildFinished(finished) => {
|
||||
if finished.success {
|
||||
log::info!("👑 Build done.");
|
||||
} else {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
_ => (), // Unknown message
|
||||
}
|
||||
}
|
||||
Ok(warning_messages)
|
||||
}
|
||||
|
||||
pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
|
||||
let crate_root = crate::cargo::crate_root().unwrap();
|
||||
let custom_html_file = crate_root.join("index.html");
|
||||
let mut html = if custom_html_file.is_file() {
|
||||
let mut buf = String::new();
|
||||
let mut file = File::open(custom_html_file).unwrap();
|
||||
if file.read_to_string(&mut buf).is_ok() {
|
||||
buf
|
||||
} else {
|
||||
String::from(include_str!("./assets/index.html"))
|
||||
}
|
||||
} else {
|
||||
String::from(include_str!("./assets/index.html"))
|
||||
};
|
||||
|
||||
let resouces = config.web.resource.clone();
|
||||
|
||||
let mut style_list = resouces.style.unwrap_or_default();
|
||||
let mut script_list = resouces.script.unwrap_or_default();
|
||||
|
||||
if serve {
|
||||
let mut dev_style = resouces.dev.style.clone().unwrap_or_default();
|
||||
let mut dev_script = resouces.dev.script.unwrap_or_default();
|
||||
style_list.append(&mut dev_style);
|
||||
script_list.append(&mut dev_script);
|
||||
}
|
||||
|
||||
let mut style_str = String::new();
|
||||
for style in style_list {
|
||||
style_str.push_str(&format!(
|
||||
"<link rel=\"stylesheet\" href=\"{}\">\n",
|
||||
&style.to_str().unwrap(),
|
||||
))
|
||||
}
|
||||
if config
|
||||
.application
|
||||
.tools
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.contains_key("tailwindcss")
|
||||
{
|
||||
style_str.push_str("<link rel=\"stylesheet\" href=\"tailwind.css\">\n");
|
||||
}
|
||||
|
||||
replace_or_insert_before("{style_include}", &style_str, "</head", &mut html);
|
||||
|
||||
let mut script_str = String::new();
|
||||
for script in script_list {
|
||||
script_str.push_str(&format!(
|
||||
"<script src=\"{}\"></script>\n",
|
||||
&script.to_str().unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
replace_or_insert_before("{script_include}", &script_str, "</body", &mut html);
|
||||
|
||||
if serve {
|
||||
html += &format!(
|
||||
"<script>{}</script>",
|
||||
include_str!("./assets/autoreload.js")
|
||||
);
|
||||
}
|
||||
|
||||
let base_path = match &config.web.app.base_path {
|
||||
Some(path) => path,
|
||||
None => ".",
|
||||
};
|
||||
let app_name = &config.application.name;
|
||||
// Check if a script already exists
|
||||
if html.contains("{app_name}") && html.contains("{base_path}") {
|
||||
html = html.replace("{app_name}", app_name);
|
||||
|
||||
html = html.replace("{base_path}", base_path);
|
||||
} else {
|
||||
// If not, insert the script
|
||||
html = html.replace(
|
||||
"</body",
|
||||
&format!(
|
||||
r#"<script type="module">
|
||||
import init from "/{base_path}/assets/dioxus/{app_name}.js";
|
||||
init("/{base_path}/assets/dioxus/{app_name}_bg.wasm").then(wasm => {{
|
||||
if (wasm.__wbindgen_start == undefined) {{
|
||||
wasm.main();
|
||||
}}
|
||||
}});
|
||||
</script>
|
||||
</body"#
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let title = config
|
||||
.web
|
||||
.app
|
||||
.title
|
||||
.clone()
|
||||
.unwrap_or_else(|| "dioxus | ⛺".into());
|
||||
|
||||
replace_or_insert_before("{app_title}", &title, "</title", &mut html);
|
||||
|
||||
html
|
||||
}
|
||||
|
||||
fn replace_or_insert_before(
|
||||
replace: &str,
|
||||
with: &str,
|
||||
or_insert_before: &str,
|
||||
content: &mut String,
|
||||
) {
|
||||
if content.contains(replace) {
|
||||
*content = content.replace(replace, with);
|
||||
} else {
|
||||
*content = content.replace(or_insert_before, &format!("{}{}", with, or_insert_before));
|
||||
}
|
||||
}
|
||||
|
||||
// this function will build some assets file
|
||||
// like sass tool resources
|
||||
// this function will return a array which file don't need copy to out_dir.
|
||||
fn build_assets(config: &CrateConfig) -> Result<Vec<PathBuf>> {
|
||||
let mut result = vec![];
|
||||
|
||||
let dioxus_config = &config.dioxus_config;
|
||||
let dioxus_tools = dioxus_config.application.tools.clone().unwrap_or_default();
|
||||
|
||||
// check sass tool state
|
||||
let sass = Tool::Sass;
|
||||
if sass.is_installed() && dioxus_tools.contains_key("sass") {
|
||||
let sass_conf = dioxus_tools.get("sass").unwrap();
|
||||
if let Some(tab) = sass_conf.as_table() {
|
||||
let source_map = tab.contains_key("source_map");
|
||||
let source_map = if source_map && tab.get("source_map").unwrap().is_bool() {
|
||||
if tab.get("source_map").unwrap().as_bool().unwrap_or_default() {
|
||||
"--source-map"
|
||||
} else {
|
||||
"--no-source-map"
|
||||
}
|
||||
} else {
|
||||
"--source-map"
|
||||
};
|
||||
|
||||
if tab.contains_key("input") {
|
||||
if tab.get("input").unwrap().is_str() {
|
||||
let file = tab.get("input").unwrap().as_str().unwrap().trim();
|
||||
|
||||
if file == "*" {
|
||||
// if the sass open auto, we need auto-check the assets dir.
|
||||
let asset_dir = config.asset_dir.clone();
|
||||
if asset_dir.is_dir() {
|
||||
for entry in walkdir::WalkDir::new(&asset_dir)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
{
|
||||
let temp = entry.path();
|
||||
if temp.is_file() {
|
||||
let suffix = temp.extension();
|
||||
if suffix.is_none() {
|
||||
continue;
|
||||
}
|
||||
let suffix = suffix.unwrap().to_str().unwrap();
|
||||
if suffix == "scss" || suffix == "sass" {
|
||||
// if file suffix is `scss` / `sass` we need transform it.
|
||||
let out_file = format!(
|
||||
"{}.css",
|
||||
temp.file_stem().unwrap().to_str().unwrap()
|
||||
);
|
||||
let target_path = config
|
||||
.out_dir
|
||||
.join(
|
||||
temp.strip_prefix(&asset_dir)
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap(),
|
||||
)
|
||||
.join(out_file);
|
||||
let res = sass.call(
|
||||
"sass",
|
||||
vec![
|
||||
temp.to_str().unwrap(),
|
||||
target_path.to_str().unwrap(),
|
||||
source_map,
|
||||
],
|
||||
);
|
||||
if res.is_ok() {
|
||||
result.push(temp.to_path_buf());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// just transform one file.
|
||||
let relative_path = if &file[0..1] == "/" {
|
||||
&file[1..file.len()]
|
||||
} else {
|
||||
file
|
||||
};
|
||||
let path = config.asset_dir.join(relative_path);
|
||||
let out_file =
|
||||
format!("{}.css", path.file_stem().unwrap().to_str().unwrap());
|
||||
let target_path = config
|
||||
.out_dir
|
||||
.join(PathBuf::from(relative_path).parent().unwrap())
|
||||
.join(out_file);
|
||||
if path.is_file() {
|
||||
let res = sass.call(
|
||||
"sass",
|
||||
vec![
|
||||
path.to_str().unwrap(),
|
||||
target_path.to_str().unwrap(),
|
||||
source_map,
|
||||
],
|
||||
);
|
||||
if res.is_ok() {
|
||||
result.push(path);
|
||||
} else {
|
||||
log::error!("{:?}", res);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if tab.get("input").unwrap().is_array() {
|
||||
// check files list.
|
||||
let list = tab.get("input").unwrap().as_array().unwrap();
|
||||
for i in list {
|
||||
if i.is_str() {
|
||||
let path = i.as_str().unwrap();
|
||||
let relative_path = if &path[0..1] == "/" {
|
||||
&path[1..path.len()]
|
||||
} else {
|
||||
path
|
||||
};
|
||||
let path = config.asset_dir.join(relative_path);
|
||||
let out_file =
|
||||
format!("{}.css", path.file_stem().unwrap().to_str().unwrap());
|
||||
let target_path = config
|
||||
.out_dir
|
||||
.join(PathBuf::from(relative_path).parent().unwrap())
|
||||
.join(out_file);
|
||||
if path.is_file() {
|
||||
let res = sass.call(
|
||||
"sass",
|
||||
vec![
|
||||
path.to_str().unwrap(),
|
||||
target_path.to_str().unwrap(),
|
||||
source_map,
|
||||
],
|
||||
);
|
||||
if res.is_ok() {
|
||||
result.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// SASS END
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// use binary_install::{Cache, Download};
|
||||
|
||||
// /// Attempts to find `wasm-opt` in `PATH` locally, or failing that downloads a
|
||||
// /// precompiled binary.
|
||||
// ///
|
||||
// /// Returns `Some` if a binary was found or it was successfully downloaded.
|
||||
// /// Returns `None` if a binary wasn't found in `PATH` and this platform doesn't
|
||||
// /// have precompiled binaries. Returns an error if we failed to download the
|
||||
// /// binary.
|
||||
// pub fn find_wasm_opt(
|
||||
// cache: &Cache,
|
||||
// install_permitted: bool,
|
||||
// ) -> Result<install::Status, failure::Error> {
|
||||
// // First attempt to look up in PATH. If found assume it works.
|
||||
// if let Ok(path) = which::which("wasm-opt") {
|
||||
// PBAR.info(&format!("found wasm-opt at {:?}", path));
|
||||
|
||||
// match path.as_path().parent() {
|
||||
// Some(path) => return Ok(install::Status::Found(Download::at(path))),
|
||||
// None => {}
|
||||
// }
|
||||
// }
|
||||
|
||||
// let version = "version_78";
|
||||
// Ok(install::download_prebuilt(
|
||||
// &install::Tool::WasmOpt,
|
||||
// cache,
|
||||
// version,
|
||||
// install_permitted,
|
||||
// )?)
|
||||
// }
|
||||
@@ -0,0 +1,94 @@
|
||||
//! Utilities for working with cargo and rust files
|
||||
use crate::error::{Error, Result};
|
||||
use std::{
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
str,
|
||||
};
|
||||
|
||||
/// How many parent folders are searched for a `Cargo.toml`
|
||||
const MAX_ANCESTORS: u32 = 10;
|
||||
|
||||
/// Some fields parsed from `cargo metadata` command
|
||||
pub struct Metadata {
|
||||
pub workspace_root: PathBuf,
|
||||
pub target_directory: PathBuf,
|
||||
}
|
||||
|
||||
/// Returns the root of the crate that the command is run from
|
||||
///
|
||||
/// If the command is run from the workspace root, this will return the top-level Cargo.toml
|
||||
pub fn crate_root() -> Result<PathBuf> {
|
||||
// From the current directory we work our way up, looking for `Cargo.toml`
|
||||
env::current_dir()
|
||||
.ok()
|
||||
.and_then(|mut wd| {
|
||||
for _ in 0..MAX_ANCESTORS {
|
||||
if contains_manifest(&wd) {
|
||||
return Some(wd);
|
||||
}
|
||||
if !wd.pop() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
Error::CargoError("Failed to find directory containing Cargo.toml".to_string())
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks if the directory contains `Cargo.toml`
|
||||
fn contains_manifest(path: &Path) -> bool {
|
||||
fs::read_dir(path)
|
||||
.map(|entries| {
|
||||
entries
|
||||
.filter_map(Result::ok)
|
||||
.any(|ent| &ent.file_name() == "Cargo.toml")
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
/// Returns the struct filled from `cargo metadata` output
|
||||
/// TODO @Jon, find a different way that doesn't rely on the cargo metadata command (it's slow)
|
||||
pub fn get() -> Result<Self> {
|
||||
let output = Command::new("cargo")
|
||||
.args(["metadata"])
|
||||
.output()
|
||||
.map_err(|_| Error::CargoError("Manifset".to_string()))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let mut msg = str::from_utf8(&output.stderr).unwrap().trim();
|
||||
if msg.starts_with("error: ") {
|
||||
msg = &msg[7..];
|
||||
}
|
||||
|
||||
return Err(Error::CargoError(msg.to_string()));
|
||||
}
|
||||
|
||||
let stdout = str::from_utf8(&output.stdout).unwrap();
|
||||
if let Some(line) = stdout.lines().next() {
|
||||
let meta: serde_json::Value = serde_json::from_str(line)
|
||||
.map_err(|_| Error::CargoError("InvalidOutput".to_string()))?;
|
||||
|
||||
let workspace_root = meta["workspace_root"]
|
||||
.as_str()
|
||||
.ok_or_else(|| Error::CargoError("InvalidOutput".to_string()))?
|
||||
.into();
|
||||
|
||||
let target_directory = meta["target_directory"]
|
||||
.as_str()
|
||||
.ok_or_else(|| Error::CargoError("InvalidOutput".to_string()))?
|
||||
.into();
|
||||
|
||||
return Ok(Self {
|
||||
workspace_root,
|
||||
target_directory,
|
||||
});
|
||||
}
|
||||
|
||||
Err(Error::CargoError("InvalidOutput".to_string()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
use futures::{stream::FuturesUnordered, StreamExt};
|
||||
use std::{fs, path::Path, process::exit};
|
||||
|
||||
use super::*;
|
||||
|
||||
// For reference, the rustfmt main.rs file
|
||||
// https://github.com/rust-lang/rustfmt/blob/master/src/bin/main.rs
|
||||
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
pub struct Autoformat {
|
||||
/// Run in 'check' mode. Exits with 0 if input is formatted correctly. Exits
|
||||
/// with 1 and prints a diff if formatting is required.
|
||||
#[clap(short, long)]
|
||||
pub check: bool,
|
||||
|
||||
/// Input rsx (selection)
|
||||
#[clap(short, long)]
|
||||
pub raw: Option<String>,
|
||||
|
||||
/// Input file
|
||||
#[clap(short, long)]
|
||||
pub file: Option<String>,
|
||||
}
|
||||
|
||||
impl Autoformat {
|
||||
// Todo: autoformat the entire crate
|
||||
pub async fn autoformat(self) -> Result<()> {
|
||||
// Default to formatting the project
|
||||
if self.raw.is_none() && self.file.is_none() {
|
||||
if let Err(e) = autoformat_project(self.check).await {
|
||||
eprintln!("error formatting project: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(raw) = self.raw {
|
||||
if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0) {
|
||||
println!("{}", inner);
|
||||
} else {
|
||||
// exit process with error
|
||||
eprintln!("error formatting codeblock");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Format single file
|
||||
if let Some(file) = self.file {
|
||||
let file_content = fs::read_to_string(&file);
|
||||
|
||||
match file_content {
|
||||
Ok(s) => {
|
||||
let edits = dioxus_autofmt::fmt_file(&s);
|
||||
let out = dioxus_autofmt::apply_formats(&s, edits);
|
||||
match fs::write(&file, out) {
|
||||
Ok(_) => {
|
||||
println!("formatted {}", file);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("failed to write formatted content to file: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("failed to open file: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Read every .rs file accessible when considering the .gitignore and try to format it
|
||||
///
|
||||
/// Runs using Tokio for multithreading, so it should be really really fast
|
||||
///
|
||||
/// Doesn't do mod-descending, so it will still try to format unreachable files. TODO.
|
||||
async fn autoformat_project(check: bool) -> Result<()> {
|
||||
let crate_config = crate::CrateConfig::new()?;
|
||||
|
||||
let mut files_to_format = vec![];
|
||||
collect_rs_files(&crate_config.crate_dir, &mut files_to_format);
|
||||
|
||||
let counts = files_to_format
|
||||
.into_iter()
|
||||
.filter(|file| {
|
||||
if file.components().any(|f| f.as_os_str() == "target") {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.map(|path| async {
|
||||
let _path = path.clone();
|
||||
let res = tokio::spawn(async move {
|
||||
let contents = tokio::fs::read_to_string(&path).await?;
|
||||
|
||||
let edits = dioxus_autofmt::fmt_file(&contents);
|
||||
let len = edits.len();
|
||||
|
||||
if !edits.is_empty() {
|
||||
let out = dioxus_autofmt::apply_formats(&contents, edits);
|
||||
tokio::fs::write(&path, out).await?;
|
||||
}
|
||||
|
||||
Ok(len) as Result<usize, tokio::io::Error>
|
||||
})
|
||||
.await;
|
||||
|
||||
if res.is_err() {
|
||||
eprintln!("error formatting file: {}", _path.display());
|
||||
}
|
||||
|
||||
res
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
let files_formatted: usize = counts
|
||||
.into_iter()
|
||||
.map(|f| match f {
|
||||
Ok(Ok(res)) => res,
|
||||
_ => 0,
|
||||
})
|
||||
.sum();
|
||||
|
||||
if files_formatted > 0 && check {
|
||||
eprintln!("{} files needed formatting", files_formatted);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_rs_files(folder: &Path, files: &mut Vec<PathBuf>) {
|
||||
let Ok(folder) = folder.read_dir() else { return };
|
||||
|
||||
// load the gitignore
|
||||
|
||||
for entry in folder {
|
||||
let Ok(entry) = entry else { continue; };
|
||||
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
collect_rs_files(&path, files);
|
||||
}
|
||||
|
||||
if let Some(ext) = path.extension() {
|
||||
if ext == "rs" {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_auto_fmt() {
|
||||
let test_rsx = r#"
|
||||
//
|
||||
|
||||
rsx! {
|
||||
|
||||
div {}
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
"#
|
||||
.to_string();
|
||||
|
||||
let fmt = Autoformat {
|
||||
check: false,
|
||||
raw: Some(test_rsx),
|
||||
file: None,
|
||||
};
|
||||
|
||||
fmt.autoformat().await.unwrap();
|
||||
}
|
||||
|
||||
/*#[test]
|
||||
fn spawn_properly() {
|
||||
let out = Command::new("dioxus")
|
||||
.args([
|
||||
"fmt",
|
||||
"-f",
|
||||
r#"
|
||||
//
|
||||
|
||||
rsx! {
|
||||
|
||||
div {}
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
"#,
|
||||
])
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
|
||||
dbg!(out);
|
||||
}*/
|
||||
@@ -0,0 +1,79 @@
|
||||
#[cfg(feature = "plugin")]
|
||||
use crate::plugin::PluginManager;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
#[clap(name = "build")]
|
||||
pub struct Build {
|
||||
#[clap(flatten)]
|
||||
pub build: ConfigOptsBuild,
|
||||
}
|
||||
|
||||
impl Build {
|
||||
pub fn build(self) -> Result<()> {
|
||||
let mut crate_config = crate::CrateConfig::new()?;
|
||||
|
||||
// change the release state.
|
||||
crate_config.with_release(self.build.release);
|
||||
crate_config.with_verbose(self.build.verbose);
|
||||
|
||||
if self.build.example.is_some() {
|
||||
crate_config.as_example(self.build.example.unwrap());
|
||||
}
|
||||
|
||||
if self.build.profile.is_some() {
|
||||
crate_config.set_profile(self.build.profile.unwrap());
|
||||
}
|
||||
|
||||
if self.build.features.is_some() {
|
||||
crate_config.set_features(self.build.features.unwrap());
|
||||
}
|
||||
|
||||
let platform = self.build.platform.unwrap_or_else(|| {
|
||||
crate_config
|
||||
.dioxus_config
|
||||
.application
|
||||
.default_platform
|
||||
.clone()
|
||||
});
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
let _ = PluginManager::on_build_start(&crate_config, &platform);
|
||||
|
||||
match platform.as_str() {
|
||||
"web" => {
|
||||
crate::builder::build(&crate_config, false)?;
|
||||
}
|
||||
"desktop" => {
|
||||
crate::builder::build_desktop(&crate_config, false)?;
|
||||
}
|
||||
_ => {
|
||||
return custom_error!("Unsupported platform target.");
|
||||
}
|
||||
}
|
||||
|
||||
let temp = gen_page(&crate_config.dioxus_config, false);
|
||||
|
||||
let mut file = std::fs::File::create(
|
||||
crate_config
|
||||
.crate_dir
|
||||
.join(
|
||||
crate_config
|
||||
.dioxus_config
|
||||
.application
|
||||
.out_dir
|
||||
.clone()
|
||||
.unwrap_or_else(|| PathBuf::from("dist")),
|
||||
)
|
||||
.join("index.html"),
|
||||
)?;
|
||||
file.write_all(temp.as_bytes())?;
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
let _ = PluginManager::on_build_finish(&crate_config, &platform);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
use super::*;
|
||||
|
||||
/// Config options for the build system.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Parser)]
|
||||
pub struct ConfigOptsBuild {
|
||||
/// The index HTML file to drive the bundling process [default: index.html]
|
||||
#[arg(long)]
|
||||
pub target: Option<PathBuf>,
|
||||
|
||||
/// Build in release mode [default: false]
|
||||
#[clap(long)]
|
||||
#[serde(default)]
|
||||
pub release: bool,
|
||||
|
||||
// Use verbose output [default: false]
|
||||
#[clap(long)]
|
||||
#[serde(default)]
|
||||
pub verbose: bool,
|
||||
|
||||
/// Build a example [default: ""]
|
||||
#[clap(long)]
|
||||
pub example: Option<String>,
|
||||
|
||||
/// Build with custom profile
|
||||
#[clap(long)]
|
||||
pub profile: Option<String>,
|
||||
|
||||
/// Build platform: support Web & Desktop [default: "default_platform"]
|
||||
#[clap(long)]
|
||||
pub platform: Option<String>,
|
||||
|
||||
/// Space separated list of features to activate
|
||||
#[clap(long)]
|
||||
pub features: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Parser)]
|
||||
pub struct ConfigOptsServe {
|
||||
/// The index HTML file to drive the bundling process [default: index.html]
|
||||
#[arg(short, long)]
|
||||
pub target: Option<PathBuf>,
|
||||
|
||||
/// Port of dev server
|
||||
#[clap(long)]
|
||||
#[clap(default_value_t = 8080)]
|
||||
pub port: u16,
|
||||
|
||||
/// Open the app in the default browser [default: false]
|
||||
#[clap(long)]
|
||||
#[serde(default)]
|
||||
pub open: bool,
|
||||
|
||||
/// Build a example [default: ""]
|
||||
#[clap(long)]
|
||||
pub example: Option<String>,
|
||||
|
||||
/// Build in release mode [default: false]
|
||||
#[clap(long)]
|
||||
#[serde(default)]
|
||||
pub release: bool,
|
||||
|
||||
// Use verbose output [default: false]
|
||||
#[clap(long)]
|
||||
#[serde(default)]
|
||||
pub verbose: bool,
|
||||
|
||||
/// Build with custom profile
|
||||
#[clap(long)]
|
||||
pub profile: Option<String>,
|
||||
|
||||
/// Build platform: support Web & Desktop [default: "default_platform"]
|
||||
#[clap(long)]
|
||||
pub platform: Option<String>,
|
||||
|
||||
/// Build with hot reloading rsx [default: false]
|
||||
#[clap(long)]
|
||||
#[serde(default)]
|
||||
pub hot_reload: bool,
|
||||
|
||||
/// Set cross-origin-policy to same-origin [default: false]
|
||||
#[clap(name = "cross-origin-policy")]
|
||||
#[clap(long)]
|
||||
#[serde(default)]
|
||||
pub cross_origin_policy: bool,
|
||||
|
||||
/// Space separated list of features to activate
|
||||
#[clap(long)]
|
||||
pub features: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// 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 { "" };
|
||||
let suffix = if !val.ends_with('/') { "/" } else { "" };
|
||||
format!("{}{}{}", prefix, val, suffix)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use super::*;
|
||||
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
#[clap(name = "clean")]
|
||||
pub struct Clean {}
|
||||
|
||||
impl Clean {
|
||||
pub fn clean(self) -> Result<()> {
|
||||
let crate_config = crate::CrateConfig::new()?;
|
||||
|
||||
let output = Command::new("cargo")
|
||||
.arg("clean")
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return custom_error!("Cargo clean failed.");
|
||||
}
|
||||
|
||||
let out_dir = crate_config
|
||||
.dioxus_config
|
||||
.application
|
||||
.out_dir
|
||||
.unwrap_or_else(|| PathBuf::from("dist"));
|
||||
if crate_config.crate_dir.join(&out_dir).is_dir() {
|
||||
remove_dir_all(crate_config.crate_dir.join(&out_dir))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
use super::*;
|
||||
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
#[derive(Clone, Debug, Deserialize, Subcommand)]
|
||||
#[clap(name = "config")]
|
||||
pub enum Config {
|
||||
/// Init `Dioxus.toml` for project/folder.
|
||||
Init {
|
||||
/// Init project name
|
||||
name: String,
|
||||
|
||||
/// Cover old config
|
||||
#[clap(long)]
|
||||
#[serde(default)]
|
||||
force: bool,
|
||||
|
||||
/// Project default platform
|
||||
#[clap(long, default_value = "web")]
|
||||
platform: String,
|
||||
},
|
||||
/// Format print Dioxus config.
|
||||
FormatPrint {},
|
||||
/// Create a custom html file.
|
||||
CustomHtml {},
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn config(self) -> Result<()> {
|
||||
let crate_root = crate::cargo::crate_root()?;
|
||||
match self {
|
||||
Config::Init {
|
||||
name,
|
||||
force,
|
||||
platform,
|
||||
} => {
|
||||
let conf_path = crate_root.join("Dioxus.toml");
|
||||
if conf_path.is_file() && !force {
|
||||
log::warn!(
|
||||
"config file `Dioxus.toml` already exist, use `--force` to overwrite it."
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
let mut file = File::create(conf_path)?;
|
||||
let content = String::from(include_str!("../../assets/dioxus.toml"))
|
||||
.replace("{{project-name}}", &name)
|
||||
.replace("{{default-platform}}", &platform);
|
||||
file.write_all(content.as_bytes())?;
|
||||
log::info!("🚩 Init config file completed.");
|
||||
}
|
||||
Config::FormatPrint {} => {
|
||||
println!("{:#?}", crate::CrateConfig::new()?.dioxus_config);
|
||||
}
|
||||
Config::CustomHtml {} => {
|
||||
let html_path = crate_root.join("index.html");
|
||||
let mut file = File::create(html_path)?;
|
||||
let content = include_str!("../../assets/index.html");
|
||||
file.write_all(content.as_bytes())?;
|
||||
log::info!("🚩 Create custom html file done.");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
use super::*;
|
||||
use cargo_generate::{GenerateArgs, TemplatePath};
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Parser)]
|
||||
#[clap(name = "create")]
|
||||
pub struct Create {
|
||||
/// Template path
|
||||
#[clap(default_value = "gh:dioxuslabs/dioxus-template", long)]
|
||||
template: String,
|
||||
}
|
||||
|
||||
impl Create {
|
||||
pub fn create(self) -> Result<()> {
|
||||
let args = GenerateArgs {
|
||||
template_path: TemplatePath {
|
||||
auto_path: Some(self.template),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let path = cargo_generate::generate(args)?;
|
||||
|
||||
// first run cargo fmt
|
||||
let mut cmd = Command::new("cargo");
|
||||
let cmd = cmd.arg("fmt").current_dir(&path);
|
||||
let output = cmd.output().expect("failed to execute process");
|
||||
if !output.status.success() {
|
||||
log::error!("cargo fmt failed");
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
|
||||
// then format the toml
|
||||
let toml_paths = [path.join("Cargo.toml"), path.join("Dioxus.toml")];
|
||||
for toml_path in &toml_paths {
|
||||
let toml = std::fs::read_to_string(toml_path)?;
|
||||
let mut toml = toml.parse::<toml_edit::Document>().map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"failed to parse toml at {}: {}",
|
||||
toml_path.display(),
|
||||
e.to_string()
|
||||
)
|
||||
})?;
|
||||
|
||||
toml.as_table_mut().fmt();
|
||||
|
||||
let as_string = toml.to_string();
|
||||
let new_string = remove_tripple_newlines(&as_string);
|
||||
let mut file = std::fs::File::create(toml_path)?;
|
||||
file.write_all(new_string.as_bytes())?;
|
||||
}
|
||||
|
||||
// remove any tripple newlines from the readme
|
||||
let readme_path = path.join("README.md");
|
||||
let readme = std::fs::read_to_string(&readme_path)?;
|
||||
let new_readme = remove_tripple_newlines(&readme);
|
||||
let mut file = std::fs::File::create(readme_path)?;
|
||||
file.write_all(new_readme.as_bytes())?;
|
||||
|
||||
log::info!("Generated project at {}", path.display());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_tripple_newlines(string: &str) -> String {
|
||||
let mut new_string = String::new();
|
||||
for char in string.chars() {
|
||||
if char == '\n' && new_string.ends_with("\n\n") {
|
||||
continue;
|
||||
}
|
||||
new_string.push(char);
|
||||
}
|
||||
new_string
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
pub mod autoformat;
|
||||
pub mod build;
|
||||
pub mod cfg;
|
||||
pub mod clean;
|
||||
pub mod config;
|
||||
pub mod create;
|
||||
pub mod plugin;
|
||||
pub mod serve;
|
||||
pub mod translate;
|
||||
pub mod version;
|
||||
|
||||
use crate::{
|
||||
cfg::{ConfigOptsBuild, ConfigOptsServe},
|
||||
custom_error,
|
||||
error::Result,
|
||||
gen_page, server, CrateConfig, Error,
|
||||
};
|
||||
use clap::{Parser, Subcommand};
|
||||
use html_parser::Dom;
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
fmt::Display,
|
||||
fs::{remove_dir_all, File},
|
||||
io::{Read, Write},
|
||||
path::PathBuf,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
/// Build, Bundle & Ship Dioxus Apps.
|
||||
#[derive(Parser)]
|
||||
#[clap(name = "dioxus", version)]
|
||||
pub struct Cli {
|
||||
#[clap(subcommand)]
|
||||
pub action: Commands,
|
||||
|
||||
/// Enable verbose logging.
|
||||
#[clap(short)]
|
||||
pub v: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub enum Commands {
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
Build(build::Build),
|
||||
|
||||
/// Translate some source file into Dioxus code.
|
||||
Translate(translate::Translate),
|
||||
|
||||
/// Build, watch & serve the Rust WASM app and all of its assets.
|
||||
Serve(serve::Serve),
|
||||
|
||||
/// Init a new project for Dioxus.
|
||||
Create(create::Create),
|
||||
|
||||
/// Clean output artifacts.
|
||||
Clean(clean::Clean),
|
||||
|
||||
/// Print the version of this extension
|
||||
#[clap(name = "version")]
|
||||
Version(version::Version),
|
||||
|
||||
/// Format some rsx
|
||||
#[clap(name = "fmt")]
|
||||
Autoformat(autoformat::Autoformat),
|
||||
|
||||
/// Dioxus config file controls.
|
||||
#[clap(subcommand)]
|
||||
Config(config::Config),
|
||||
|
||||
/// Manage plugins for dioxus cli
|
||||
#[cfg(feature = "plugin")]
|
||||
#[clap(subcommand)]
|
||||
Plugin(plugin::Plugin),
|
||||
}
|
||||
|
||||
impl Display for Commands {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Commands::Build(_) => write!(f, "build"),
|
||||
Commands::Translate(_) => write!(f, "translate"),
|
||||
Commands::Serve(_) => write!(f, "serve"),
|
||||
Commands::Create(_) => write!(f, "create"),
|
||||
Commands::Clean(_) => write!(f, "clean"),
|
||||
Commands::Config(_) => write!(f, "config"),
|
||||
Commands::Version(_) => write!(f, "version"),
|
||||
Commands::Autoformat(_) => write!(f, "fmt"),
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
Commands::Plugin(_) => write!(f, "plugin"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#![cfg(feature = "plugin")]
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
#[derive(Clone, Debug, Deserialize, Subcommand)]
|
||||
#[clap(name = "plugin")]
|
||||
pub enum Plugin {
|
||||
/// Return all dioxus-cli support tools.
|
||||
List {},
|
||||
/// Get default app install path.
|
||||
AppPath {},
|
||||
/// Install a new tool.
|
||||
Add { name: String },
|
||||
}
|
||||
|
||||
impl Plugin {
|
||||
pub async fn plugin(self) -> Result<()> {
|
||||
match self {
|
||||
Plugin::List {} => {
|
||||
for item in crate::plugin::PluginManager::plugin_list() {
|
||||
println!("- {item}");
|
||||
}
|
||||
}
|
||||
Plugin::AppPath {} => {
|
||||
let plugin_dir = crate::plugin::PluginManager::init_plugin_dir();
|
||||
if let Some(v) = plugin_dir.to_str() {
|
||||
println!("{}", v);
|
||||
} else {
|
||||
log::error!("Plugin path get failed.");
|
||||
}
|
||||
}
|
||||
Plugin::Add { name: _ } => {
|
||||
log::info!("You can use `dioxus plugin app-path` to get Installation position");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
use super::*;
|
||||
use std::{
|
||||
fs::create_dir_all,
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
/// Run the WASM project on dev-server
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
#[clap(name = "serve")]
|
||||
pub struct Serve {
|
||||
#[clap(flatten)]
|
||||
pub serve: ConfigOptsServe,
|
||||
}
|
||||
|
||||
impl Serve {
|
||||
pub async fn serve(self) -> Result<()> {
|
||||
let mut crate_config = crate::CrateConfig::new()?;
|
||||
|
||||
// change the relase state.
|
||||
crate_config.with_hot_reload(self.serve.hot_reload);
|
||||
crate_config.with_cross_origin_policy(self.serve.cross_origin_policy);
|
||||
crate_config.with_release(self.serve.release);
|
||||
crate_config.with_verbose(self.serve.verbose);
|
||||
|
||||
if self.serve.example.is_some() {
|
||||
crate_config.as_example(self.serve.example.unwrap());
|
||||
}
|
||||
|
||||
if self.serve.profile.is_some() {
|
||||
crate_config.set_profile(self.serve.profile.unwrap());
|
||||
}
|
||||
|
||||
if self.serve.features.is_some() {
|
||||
crate_config.set_features(self.serve.features.unwrap());
|
||||
}
|
||||
|
||||
// Subdirectories don't work with the server
|
||||
crate_config.dioxus_config.web.app.base_path = None;
|
||||
|
||||
let platform = self.serve.platform.unwrap_or_else(|| {
|
||||
crate_config
|
||||
.dioxus_config
|
||||
.application
|
||||
.default_platform
|
||||
.clone()
|
||||
});
|
||||
|
||||
if platform.as_str() == "desktop" {
|
||||
crate::builder::build_desktop(&crate_config, true)?;
|
||||
|
||||
match &crate_config.executable {
|
||||
crate::ExecutableType::Binary(name)
|
||||
| crate::ExecutableType::Lib(name)
|
||||
| crate::ExecutableType::Example(name) => {
|
||||
let mut file = crate_config.out_dir.join(name);
|
||||
if cfg!(windows) {
|
||||
file.set_extension("exe");
|
||||
}
|
||||
Command::new(file.to_str().unwrap())
|
||||
.stdout(Stdio::inherit())
|
||||
.output()?;
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
} else if platform != "web" {
|
||||
return custom_error!("Unsupported platform target.");
|
||||
}
|
||||
|
||||
// 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(())
|
||||
}
|
||||
|
||||
pub fn regen_dev_page(crate_config: &CrateConfig) -> Result<()> {
|
||||
let serve_html = gen_page(&crate_config.dioxus_config, true);
|
||||
|
||||
let dist_path = crate_config.crate_dir.join(
|
||||
crate_config
|
||||
.dioxus_config
|
||||
.application
|
||||
.out_dir
|
||||
.clone()
|
||||
.unwrap_or_else(|| PathBuf::from("dist")),
|
||||
);
|
||||
if !dist_path.is_dir() {
|
||||
create_dir_all(&dist_path)?;
|
||||
}
|
||||
let index_path = dist_path.join("index.html");
|
||||
let mut file = std::fs::File::create(index_path)?;
|
||||
file.write_all(serve_html.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
use crate::tools;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
#[derive(Clone, Debug, Deserialize, Subcommand)]
|
||||
#[clap(name = "tool")]
|
||||
pub enum Tool {
|
||||
/// Return all dioxus-cli support tools.
|
||||
List {},
|
||||
/// Get default app install path.
|
||||
AppPath {},
|
||||
/// Install a new tool.
|
||||
Add { name: String },
|
||||
}
|
||||
|
||||
impl Tool {
|
||||
pub async fn tool(self) -> Result<()> {
|
||||
match self {
|
||||
Tool::List {} => {
|
||||
for item in tools::tool_list() {
|
||||
if tools::Tool::from_str(item).unwrap().is_installed() {
|
||||
println!("- {item} [installed]");
|
||||
} else {
|
||||
println!("- {item}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Tool::AppPath {} => {
|
||||
if let Some(v) = tools::tools_path().to_str() {
|
||||
println!("{}", v);
|
||||
} else {
|
||||
return custom_error!("Tools path get failed.");
|
||||
}
|
||||
}
|
||||
Tool::Add { name } => {
|
||||
let tool_list = tools::tool_list();
|
||||
|
||||
if !tool_list.contains(&name.as_str()) {
|
||||
return custom_error!("Tool {name} not found.");
|
||||
}
|
||||
let target_tool = tools::Tool::from_str(&name).unwrap();
|
||||
|
||||
if target_tool.is_installed() {
|
||||
log::warn!("Tool {name} is installed.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::info!("Start to download tool package...");
|
||||
if let Err(e) = target_tool.download_package().await {
|
||||
return custom_error!("Tool download failed: {e}");
|
||||
}
|
||||
|
||||
log::info!("Start to install tool package...");
|
||||
if let Err(e) = target_tool.install_package().await {
|
||||
return custom_error!("Tool install failed: {e}");
|
||||
}
|
||||
|
||||
log::info!("Tool {name} installed successfully!");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
use std::process::exit;
|
||||
|
||||
use dioxus_rsx::{BodyNode, CallBody};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
#[clap(name = "translate")]
|
||||
pub struct Translate {
|
||||
/// Activate debug mode
|
||||
// short and long flags (-d, --debug) will be deduced from the field's name
|
||||
#[clap(short, long)]
|
||||
pub component: bool,
|
||||
|
||||
/// Input file
|
||||
#[clap(short, long)]
|
||||
pub file: Option<String>,
|
||||
|
||||
/// Input file
|
||||
#[clap(short, long)]
|
||||
pub raw: Option<String>,
|
||||
|
||||
/// Output file, stdout if not present
|
||||
#[arg(short, long)]
|
||||
pub output: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Translate {
|
||||
pub fn translate(self) -> Result<()> {
|
||||
// Get the right input for the translation
|
||||
let contents = determine_input(self.file, self.raw)?;
|
||||
|
||||
// Ensure we're loading valid HTML
|
||||
let dom = html_parser::Dom::parse(&contents)?;
|
||||
|
||||
// Convert the HTML to RSX
|
||||
let out = convert_html_to_formatted_rsx(&dom, self.component);
|
||||
|
||||
// Write the output
|
||||
match self.output {
|
||||
Some(output) => std::fs::write(output, out)?,
|
||||
None => print!("{}", out),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert_html_to_formatted_rsx(dom: &Dom, component: bool) -> String {
|
||||
let callbody = rsx_rosetta::rsx_from_html(dom);
|
||||
|
||||
match component {
|
||||
true => write_callbody_with_icon_section(callbody),
|
||||
false => dioxus_autofmt::write_block_out(callbody).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_callbody_with_icon_section(mut callbody: CallBody) -> String {
|
||||
let mut svgs = vec![];
|
||||
|
||||
rsx_rosetta::collect_svgs(&mut callbody.roots, &mut svgs);
|
||||
|
||||
let mut out = write_component_body(dioxus_autofmt::write_block_out(callbody).unwrap());
|
||||
|
||||
if !svgs.is_empty() {
|
||||
write_svg_section(&mut out, svgs);
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn write_component_body(raw: String) -> String {
|
||||
let mut out = String::from("fn component(cx: Scope) -> Element {\n cx.render(rsx! {");
|
||||
indent_and_write(&raw, 1, &mut out);
|
||||
out.push_str(" })\n}");
|
||||
out
|
||||
}
|
||||
|
||||
fn write_svg_section(out: &mut String, svgs: Vec<BodyNode>) {
|
||||
out.push_str("\n\nmod icons {");
|
||||
out.push_str("\n use super::*;");
|
||||
for (idx, icon) in svgs.into_iter().enumerate() {
|
||||
let raw = dioxus_autofmt::write_block_out(CallBody { roots: vec![icon] }).unwrap();
|
||||
out.push_str("\n\n pub fn icon_");
|
||||
out.push_str(&idx.to_string());
|
||||
out.push_str("(cx: Scope) -> Element {\n cx.render(rsx! {");
|
||||
indent_and_write(&raw, 2, out);
|
||||
out.push_str(" })\n }");
|
||||
}
|
||||
|
||||
out.push_str("\n}");
|
||||
}
|
||||
|
||||
fn indent_and_write(raw: &str, idx: usize, out: &mut String) {
|
||||
for line in raw.lines() {
|
||||
for _ in 0..idx {
|
||||
out.push_str(" ");
|
||||
}
|
||||
out.push_str(line);
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
fn determine_input(file: Option<String>, raw: Option<String>) -> Result<String> {
|
||||
// Make sure not both are specified
|
||||
if file.is_some() && raw.is_some() {
|
||||
log::error!("Only one of --file or --raw should be specified.");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if let Some(raw) = raw {
|
||||
return Ok(raw);
|
||||
}
|
||||
|
||||
if let Some(file) = file {
|
||||
return Ok(std::fs::read_to_string(file)?);
|
||||
}
|
||||
|
||||
// If neither exist, we try to read from stdin
|
||||
if atty::is(atty::Stream::Stdin) {
|
||||
return custom_error!("No input file, source, or stdin to translate from.");
|
||||
}
|
||||
|
||||
let mut buffer = String::new();
|
||||
std::io::stdin().read_to_string(&mut buffer).unwrap();
|
||||
|
||||
Ok(buffer.trim().to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generates_svgs() {
|
||||
let st = include_str!("../../../tests/svg.html");
|
||||
|
||||
let out = convert_html_to_formatted_rsx(&html_parser::Dom::parse(st).unwrap(), true);
|
||||
|
||||
println!("{}", out);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
use super::*;
|
||||
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
#[clap(name = "version")]
|
||||
pub struct Version {}
|
||||
|
||||
impl Version {
|
||||
pub fn version(self) -> VersionInfo {
|
||||
version()
|
||||
}
|
||||
}
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// Information about the git repository where rust-analyzer was built from.
|
||||
pub struct CommitInfo {
|
||||
pub short_commit_hash: &'static str,
|
||||
pub commit_hash: &'static str,
|
||||
pub commit_date: &'static str,
|
||||
}
|
||||
|
||||
/// Cargo's version.
|
||||
pub struct VersionInfo {
|
||||
/// rust-analyzer's version, such as "1.57.0", "1.58.0-beta.1", "1.59.0-nightly", etc.
|
||||
pub version: &'static str,
|
||||
|
||||
/// The release channel we were built for (stable/beta/nightly/dev).
|
||||
///
|
||||
/// `None` if not built via rustbuild.
|
||||
pub release_channel: Option<&'static str>,
|
||||
|
||||
/// Information about the Git repository we may have been built from.
|
||||
///
|
||||
/// `None` if not built from a git repo.
|
||||
pub commit_info: Option<CommitInfo>,
|
||||
}
|
||||
|
||||
impl fmt::Display for VersionInfo {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.version)?;
|
||||
|
||||
if let Some(ci) = &self.commit_info {
|
||||
write!(f, " ({} {})", ci.short_commit_hash, ci.commit_date)?;
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns information about cargo's version.
|
||||
pub const fn version() -> VersionInfo {
|
||||
let version = match option_env!("CFG_RELEASE") {
|
||||
Some(x) => x,
|
||||
None => "0.0.0",
|
||||
};
|
||||
|
||||
let release_channel = option_env!("CFG_RELEASE_CHANNEL");
|
||||
let commit_info = match (
|
||||
option_env!("RA_COMMIT_SHORT_HASH"),
|
||||
option_env!("RA_COMMIT_HASH"),
|
||||
option_env!("RA_COMMIT_DATE"),
|
||||
) {
|
||||
(Some(short_commit_hash), Some(commit_hash), Some(commit_date)) => Some(CommitInfo {
|
||||
short_commit_hash,
|
||||
commit_hash,
|
||||
commit_date,
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
VersionInfo {
|
||||
version,
|
||||
release_channel,
|
||||
commit_info,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
use crate::error::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DioxusConfig {
|
||||
pub application: ApplicationConfig,
|
||||
|
||||
pub web: WebConfig,
|
||||
|
||||
#[serde(default = "default_plugin")]
|
||||
pub plugin: toml::Value,
|
||||
}
|
||||
|
||||
fn default_plugin() -> toml::Value {
|
||||
toml::Value::Boolean(true)
|
||||
}
|
||||
|
||||
impl DioxusConfig {
|
||||
pub fn load() -> crate::error::Result<Option<DioxusConfig>> {
|
||||
let Ok(crate_dir) = crate::cargo::crate_root() else { return Ok(None); };
|
||||
|
||||
// we support either `Dioxus.toml` or `Cargo.toml`
|
||||
let Some(dioxus_conf_file) = acquire_dioxus_toml(crate_dir) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
toml::from_str::<DioxusConfig>(&std::fs::read_to_string(dioxus_conf_file)?)
|
||||
.map_err(|_| crate::Error::Unique("Dioxus.toml parse failed".into()))
|
||||
.map(Some)
|
||||
}
|
||||
}
|
||||
|
||||
fn acquire_dioxus_toml(dir: PathBuf) -> Option<PathBuf> {
|
||||
// prefer uppercase
|
||||
if dir.join("Dioxus.toml").is_file() {
|
||||
return Some(dir.join("Dioxus.toml"));
|
||||
}
|
||||
|
||||
// lowercase is fine too
|
||||
if dir.join("dioxus.toml").is_file() {
|
||||
return Some(dir.join("Dioxus.toml"));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
impl Default for DioxusConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
application: ApplicationConfig {
|
||||
name: "dioxus".into(),
|
||||
default_platform: "web".to_string(),
|
||||
out_dir: Some(PathBuf::from("dist")),
|
||||
asset_dir: Some(PathBuf::from("public")),
|
||||
|
||||
tools: None,
|
||||
|
||||
sub_package: None,
|
||||
},
|
||||
web: WebConfig {
|
||||
app: WebAppConfig {
|
||||
title: Some("dioxus | ⛺".into()),
|
||||
base_path: None,
|
||||
},
|
||||
proxy: Some(vec![]),
|
||||
watcher: WebWatcherConfig {
|
||||
watch_path: Some(vec![PathBuf::from("src")]),
|
||||
reload_html: Some(false),
|
||||
index_on_404: Some(true),
|
||||
},
|
||||
resource: WebResourceConfig {
|
||||
dev: WebDevResourceConfig {
|
||||
style: Some(vec![]),
|
||||
script: Some(vec![]),
|
||||
},
|
||||
style: Some(vec![]),
|
||||
script: Some(vec![]),
|
||||
},
|
||||
https: WebHttpsConfig {
|
||||
enabled: None,
|
||||
mkcert: None,
|
||||
key_path: None,
|
||||
cert_path: None,
|
||||
},
|
||||
},
|
||||
plugin: toml::Value::Table(toml::map::Map::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ApplicationConfig {
|
||||
pub name: String,
|
||||
pub default_platform: String,
|
||||
pub out_dir: Option<PathBuf>,
|
||||
pub asset_dir: Option<PathBuf>,
|
||||
|
||||
pub tools: Option<HashMap<String, toml::Value>>,
|
||||
|
||||
pub sub_package: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WebConfig {
|
||||
pub app: WebAppConfig,
|
||||
pub proxy: Option<Vec<WebProxyConfig>>,
|
||||
pub watcher: WebWatcherConfig,
|
||||
pub resource: WebResourceConfig,
|
||||
#[serde(default)]
|
||||
pub https: WebHttpsConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WebAppConfig {
|
||||
pub title: Option<String>,
|
||||
pub base_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WebProxyConfig {
|
||||
pub backend: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WebWatcherConfig {
|
||||
pub watch_path: Option<Vec<PathBuf>>,
|
||||
pub reload_html: Option<bool>,
|
||||
pub index_on_404: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WebResourceConfig {
|
||||
pub dev: WebDevResourceConfig,
|
||||
pub style: Option<Vec<PathBuf>>,
|
||||
pub script: Option<Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WebDevResourceConfig {
|
||||
pub style: Option<Vec<PathBuf>>,
|
||||
pub script: Option<Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
pub struct WebHttpsConfig {
|
||||
pub enabled: Option<bool>,
|
||||
pub mkcert: Option<bool>,
|
||||
pub key_path: Option<String>,
|
||||
pub cert_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CrateConfig {
|
||||
pub out_dir: PathBuf,
|
||||
pub crate_dir: PathBuf,
|
||||
pub workspace_dir: PathBuf,
|
||||
pub target_dir: PathBuf,
|
||||
pub asset_dir: PathBuf,
|
||||
pub manifest: cargo_toml::Manifest<cargo_toml::Value>,
|
||||
pub executable: ExecutableType,
|
||||
pub dioxus_config: DioxusConfig,
|
||||
pub release: bool,
|
||||
pub hot_reload: bool,
|
||||
pub cross_origin_policy: bool,
|
||||
pub verbose: bool,
|
||||
pub custom_profile: Option<String>,
|
||||
pub features: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExecutableType {
|
||||
Binary(String),
|
||||
Lib(String),
|
||||
Example(String),
|
||||
}
|
||||
|
||||
impl CrateConfig {
|
||||
pub fn new() -> Result<Self> {
|
||||
let dioxus_config = DioxusConfig::load()?.unwrap_or_default();
|
||||
|
||||
let crate_dir = if let Some(package) = &dioxus_config.application.sub_package {
|
||||
crate::cargo::crate_root()?.join(package)
|
||||
} else {
|
||||
crate::cargo::crate_root()?
|
||||
};
|
||||
let meta = crate::cargo::Metadata::get()?;
|
||||
let workspace_dir = meta.workspace_root;
|
||||
let target_dir = meta.target_directory;
|
||||
|
||||
let out_dir = match dioxus_config.application.out_dir {
|
||||
Some(ref v) => crate_dir.join(v),
|
||||
None => crate_dir.join("dist"),
|
||||
};
|
||||
|
||||
let cargo_def = &crate_dir.join("Cargo.toml");
|
||||
|
||||
let asset_dir = match dioxus_config.application.asset_dir {
|
||||
Some(ref v) => crate_dir.join(v),
|
||||
None => crate_dir.join("public"),
|
||||
};
|
||||
|
||||
let manifest = cargo_toml::Manifest::from_path(cargo_def).unwrap();
|
||||
|
||||
let output_filename = {
|
||||
match &manifest.package.as_ref().unwrap().default_run {
|
||||
Some(default_run_target) => default_run_target.to_owned(),
|
||||
None => manifest
|
||||
.bin
|
||||
.iter()
|
||||
.find(|b| b.name == manifest.package.as_ref().map(|pkg| pkg.name.clone()))
|
||||
.or(manifest
|
||||
.bin
|
||||
.iter()
|
||||
.find(|b| b.path == Some("src/main.rs".to_owned())))
|
||||
.or(manifest.bin.first())
|
||||
.or(manifest.lib.as_ref())
|
||||
.and_then(|prod| prod.name.clone())
|
||||
.expect("No executable or library found from cargo metadata."),
|
||||
}
|
||||
};
|
||||
let executable = ExecutableType::Binary(output_filename);
|
||||
|
||||
let release = false;
|
||||
let hot_reload = false;
|
||||
let verbose = false;
|
||||
let custom_profile = None;
|
||||
let features = None;
|
||||
|
||||
Ok(Self {
|
||||
out_dir,
|
||||
crate_dir,
|
||||
workspace_dir,
|
||||
target_dir,
|
||||
asset_dir,
|
||||
manifest,
|
||||
executable,
|
||||
release,
|
||||
dioxus_config,
|
||||
hot_reload,
|
||||
cross_origin_policy: false,
|
||||
custom_profile,
|
||||
features,
|
||||
verbose,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn as_example(&mut self, example_name: String) -> &mut Self {
|
||||
self.executable = ExecutableType::Example(example_name);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_release(&mut self, release: bool) -> &mut Self {
|
||||
self.release = release;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_hot_reload(&mut self, hot_reload: bool) -> &mut Self {
|
||||
self.hot_reload = hot_reload;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_cross_origin_policy(&mut self, cross_origin_policy: bool) -> &mut Self {
|
||||
self.cross_origin_policy = cross_origin_policy;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_verbose(&mut self, verbose: bool) -> &mut Self {
|
||||
self.verbose = verbose;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_profile(&mut self, profile: String) -> &mut Self {
|
||||
self.custom_profile = Some(profile);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_features(&mut self, features: Vec<String>) -> &mut Self {
|
||||
self.features = Some(features);
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
use thiserror::Error as ThisError;
|
||||
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
#[derive(ThisError, Debug)]
|
||||
pub enum Error {
|
||||
/// Used when errors need to propogate but are too unique to be typed
|
||||
#[error("{0}")]
|
||||
Unique(String),
|
||||
|
||||
#[error("I/O Error: {0}")]
|
||||
IO(#[from] std::io::Error),
|
||||
|
||||
#[error("Format Error: {0}")]
|
||||
FormatError(#[from] std::fmt::Error),
|
||||
|
||||
#[error("Format failed: {0}")]
|
||||
ParseError(String),
|
||||
|
||||
#[error("Runtime Error: {0}")]
|
||||
RuntimeError(String),
|
||||
|
||||
#[error("Failed to write error")]
|
||||
FailedToWrite,
|
||||
|
||||
#[error("Build Failed: {0}")]
|
||||
BuildFailed(String),
|
||||
|
||||
#[error("Cargo Error: {0}")]
|
||||
CargoError(String),
|
||||
|
||||
#[error("{0}")]
|
||||
CustomError(String),
|
||||
|
||||
#[error("Invalid proxy URL: {0}")]
|
||||
InvalidProxy(#[from] hyper::http::uri::InvalidUri),
|
||||
|
||||
#[error("Error proxying request: {0}")]
|
||||
ProxyRequestError(hyper::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
impl From<&str> for Error {
|
||||
fn from(s: &str) -> Self {
|
||||
Error::Unique(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(s: String) -> Self {
|
||||
Error::Unique(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<html_parser::Error> for Error {
|
||||
fn from(e: html_parser::Error) -> Self {
|
||||
Self::ParseError(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<hyper::Error> for Error {
|
||||
fn from(e: hyper::Error) -> Self {
|
||||
Self::RuntimeError(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! custom_error {
|
||||
($msg:literal $(,)?) => {
|
||||
Err(Error::CustomError(format!($msg)))
|
||||
};
|
||||
($err:expr $(,)?) => {
|
||||
Err(Error::from($err))
|
||||
};
|
||||
($fmt:expr, $($arg:tt)*) => {
|
||||
Err(Error::CustomError(format!($fmt, $($arg)*)))
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
pub const DIOXUS_CLI_VERSION: &str = "0.1.5";
|
||||
|
||||
pub mod builder;
|
||||
pub mod server;
|
||||
pub mod tools;
|
||||
|
||||
pub use builder::*;
|
||||
|
||||
pub mod cargo;
|
||||
pub use cargo::*;
|
||||
|
||||
pub mod cli;
|
||||
pub use cli::*;
|
||||
|
||||
pub mod config;
|
||||
pub use config::*;
|
||||
|
||||
pub mod error;
|
||||
pub use error::*;
|
||||
|
||||
pub mod logging;
|
||||
pub use logging::*;
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
pub mod plugin;
|
||||
@@ -0,0 +1,35 @@
|
||||
use fern::colors::{Color, ColoredLevelConfig};
|
||||
|
||||
pub fn set_up_logging() {
|
||||
// configure colors for the whole line
|
||||
let colors_line = ColoredLevelConfig::new()
|
||||
.error(Color::Red)
|
||||
.warn(Color::Yellow)
|
||||
// we actually don't need to specify the color for debug and info, they are white by default
|
||||
.info(Color::White)
|
||||
.debug(Color::White)
|
||||
// depending on the terminals color scheme, this is the same as the background color
|
||||
.trace(Color::BrightBlack);
|
||||
|
||||
// configure colors for the name of the level.
|
||||
// since almost all of them are the same as the color for the whole line, we
|
||||
// just clone `colors_line` and overwrite our changes
|
||||
let colors_level = colors_line.info(Color::Green);
|
||||
// here we set up our fern Dispatch
|
||||
fern::Dispatch::new()
|
||||
.format(move |out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"{color_line}[{level}{color_line}] {message}\x1B[0m",
|
||||
color_line = format_args!(
|
||||
"\x1B[{}m",
|
||||
colors_line.get_color(&record.level()).to_fg_str()
|
||||
),
|
||||
level = colors_level.color(record.level()),
|
||||
message = message,
|
||||
));
|
||||
})
|
||||
.level(log::LevelFilter::Info)
|
||||
.chain(std::io::stdout())
|
||||
.apply()
|
||||
.unwrap();
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use anyhow::anyhow;
|
||||
use clap::Parser;
|
||||
use dioxus_cli::*;
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
use dioxus_cli::plugin::PluginManager;
|
||||
|
||||
use Commands::*;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let args = Cli::parse();
|
||||
|
||||
set_up_logging();
|
||||
|
||||
let _dioxus_config = DioxusConfig::load()
|
||||
.map_err(|e| anyhow!("Failed to load `Dioxus.toml` because: {e}"))?
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!("You appear to be creating a Dioxus project from scratch; we will use the default config");
|
||||
DioxusConfig::default()
|
||||
});
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
PluginManager::init(_dioxus_config.plugin)
|
||||
.map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?;
|
||||
|
||||
match args.action {
|
||||
Translate(opts) => opts
|
||||
.translate()
|
||||
.map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)),
|
||||
|
||||
Build(opts) => opts
|
||||
.build()
|
||||
.map_err(|e| anyhow!("🚫 Building project failed: {}", e)),
|
||||
|
||||
Clean(opts) => opts
|
||||
.clean()
|
||||
.map_err(|e| anyhow!("🚫 Cleaning project failed: {}", e)),
|
||||
|
||||
Serve(opts) => opts
|
||||
.serve()
|
||||
.await
|
||||
.map_err(|e| anyhow!("🚫 Serving project failed: {}", e)),
|
||||
|
||||
Create(opts) => opts
|
||||
.create()
|
||||
.map_err(|e| anyhow!("🚫 Creating new project failed: {}", e)),
|
||||
|
||||
Config(opts) => opts
|
||||
.config()
|
||||
.map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)),
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
Plugin(opts) => opts
|
||||
.plugin()
|
||||
.await
|
||||
.map_err(|e| anyhow!("🚫 Error with plugin: {}", e)),
|
||||
|
||||
Autoformat(opts) => opts
|
||||
.autoformat()
|
||||
.await
|
||||
.map_err(|e| anyhow!("🚫 Error autoformatting RSX: {}", e)),
|
||||
|
||||
Version(opt) => {
|
||||
let version = opt.version();
|
||||
println!("{}", version);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use mlua::{FromLua, UserData};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum StdioFromString {
|
||||
Inherit,
|
||||
Piped,
|
||||
Null,
|
||||
}
|
||||
impl<'lua> FromLua<'lua> for StdioFromString {
|
||||
fn from_lua(lua_value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result<Self> {
|
||||
if let mlua::Value::String(v) = lua_value {
|
||||
let v = v.to_str().unwrap();
|
||||
return Ok(match v.to_lowercase().as_str() {
|
||||
"inherit" => Self::Inherit,
|
||||
"piped" => Self::Piped,
|
||||
"null" => Self::Null,
|
||||
_ => Self::Inherit,
|
||||
});
|
||||
}
|
||||
Ok(Self::Inherit)
|
||||
}
|
||||
}
|
||||
impl StdioFromString {
|
||||
pub fn to_stdio(self) -> Stdio {
|
||||
match self {
|
||||
StdioFromString::Inherit => Stdio::inherit(),
|
||||
StdioFromString::Piped => Stdio::piped(),
|
||||
StdioFromString::Null => Stdio::null(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PluginCommander;
|
||||
impl UserData for PluginCommander {
|
||||
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_function(
|
||||
"exec",
|
||||
|_, args: (Vec<String>, StdioFromString, StdioFromString)| {
|
||||
let cmd = args.0;
|
||||
let stdout = args.1;
|
||||
let stderr = args.2;
|
||||
|
||||
if cmd.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let cmd_name = cmd.get(0).unwrap();
|
||||
let mut command = Command::new(cmd_name);
|
||||
let t = cmd
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| *i > 0)
|
||||
.map(|v| v.1.clone())
|
||||
.collect::<Vec<String>>();
|
||||
command.args(t);
|
||||
command.stdout(stdout.to_stdio()).stderr(stderr.to_stdio());
|
||||
command.output()?;
|
||||
Ok(())
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(_fields: &mut F) {}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
use mlua::UserData;
|
||||
|
||||
use crate::tools::app_path;
|
||||
|
||||
pub struct PluginDirs;
|
||||
impl UserData for PluginDirs {
|
||||
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_function("plugins_dir", |_, ()| {
|
||||
let path = app_path().join("plugins");
|
||||
Ok(path.to_str().unwrap().to_string())
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
use std::{
|
||||
fs::{create_dir, create_dir_all, remove_dir_all, File},
|
||||
io::{Read, Write},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use crate::tools::extract_zip;
|
||||
use flate2::read::GzDecoder;
|
||||
use mlua::UserData;
|
||||
use tar::Archive;
|
||||
|
||||
pub struct PluginFileSystem;
|
||||
impl UserData for PluginFileSystem {
|
||||
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_function("create_dir", |_, args: (String, bool)| {
|
||||
let path = args.0;
|
||||
let recursive = args.1;
|
||||
let path = PathBuf::from(path);
|
||||
if !path.exists() {
|
||||
let v = if recursive {
|
||||
create_dir_all(path)
|
||||
} else {
|
||||
create_dir(path)
|
||||
};
|
||||
return Ok(v.is_ok());
|
||||
}
|
||||
Ok(true)
|
||||
});
|
||||
methods.add_function("remove_dir", |_, path: String| {
|
||||
let path = PathBuf::from(path);
|
||||
let r = remove_dir_all(path);
|
||||
Ok(r.is_ok())
|
||||
});
|
||||
methods.add_function("file_get_content", |_, path: String| {
|
||||
let path = PathBuf::from(path);
|
||||
let mut file = std::fs::File::open(path)?;
|
||||
let mut buffer = String::new();
|
||||
file.read_to_string(&mut buffer)?;
|
||||
Ok(buffer)
|
||||
});
|
||||
methods.add_function("file_set_content", |_, args: (String, String)| {
|
||||
let path = args.0;
|
||||
let content = args.1;
|
||||
let path = PathBuf::from(path);
|
||||
|
||||
let file = std::fs::File::create(path);
|
||||
if file.is_err() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if file.unwrap().write_all(content.as_bytes()).is_err() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
});
|
||||
methods.add_function("unzip_file", |_, args: (String, String)| {
|
||||
let file = PathBuf::from(args.0);
|
||||
let target = PathBuf::from(args.1);
|
||||
let res = extract_zip(&file, &target);
|
||||
if res.is_err() {
|
||||
return Ok(false);
|
||||
}
|
||||
Ok(true)
|
||||
});
|
||||
methods.add_function("untar_gz_file", |_, args: (String, String)| {
|
||||
let file = PathBuf::from(args.0);
|
||||
let target = PathBuf::from(args.1);
|
||||
|
||||
let tar_gz = if let Ok(v) = File::open(file) {
|
||||
v
|
||||
} else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let tar = GzDecoder::new(tar_gz);
|
||||
let mut archive = Archive::new(tar);
|
||||
if archive.unpack(&target).is_err() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use log;
|
||||
use mlua::UserData;
|
||||
|
||||
pub struct PluginLogger;
|
||||
impl UserData for PluginLogger {
|
||||
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_function("trace", |_, info: String| {
|
||||
log::trace!("{}", info);
|
||||
Ok(())
|
||||
});
|
||||
methods.add_function("info", |_, info: String| {
|
||||
log::info!("{}", info);
|
||||
Ok(())
|
||||
});
|
||||
methods.add_function("debug", |_, info: String| {
|
||||
log::debug!("{}", info);
|
||||
Ok(())
|
||||
});
|
||||
methods.add_function("warn", |_, info: String| {
|
||||
log::warn!("{}", info);
|
||||
Ok(())
|
||||
});
|
||||
methods.add_function("error", |_, info: String| {
|
||||
log::error!("{}", info);
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
use mlua::{FromLua, Function, ToLua};
|
||||
|
||||
pub mod command;
|
||||
pub mod dirs;
|
||||
pub mod fs;
|
||||
pub mod log;
|
||||
pub mod network;
|
||||
pub mod os;
|
||||
pub mod path;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PluginInfo<'lua> {
|
||||
pub name: String,
|
||||
pub repository: String,
|
||||
pub author: String,
|
||||
pub version: String,
|
||||
|
||||
pub inner: PluginInner,
|
||||
|
||||
pub on_init: Option<Function<'lua>>,
|
||||
pub build: PluginBuildInfo<'lua>,
|
||||
pub serve: PluginServeInfo<'lua>,
|
||||
}
|
||||
|
||||
impl<'lua> FromLua<'lua> for PluginInfo<'lua> {
|
||||
fn from_lua(lua_value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result<Self> {
|
||||
let mut res = Self {
|
||||
name: String::default(),
|
||||
repository: String::default(),
|
||||
author: String::default(),
|
||||
version: String::from("0.1.0"),
|
||||
|
||||
inner: Default::default(),
|
||||
|
||||
on_init: None,
|
||||
build: Default::default(),
|
||||
serve: Default::default(),
|
||||
};
|
||||
if let mlua::Value::Table(tab) = lua_value {
|
||||
if let Ok(v) = tab.get::<_, String>("name") {
|
||||
res.name = v;
|
||||
}
|
||||
if let Ok(v) = tab.get::<_, String>("repository") {
|
||||
res.repository = v;
|
||||
}
|
||||
if let Ok(v) = tab.get::<_, String>("author") {
|
||||
res.author = v;
|
||||
}
|
||||
if let Ok(v) = tab.get::<_, String>("version") {
|
||||
res.version = v;
|
||||
}
|
||||
|
||||
if let Ok(v) = tab.get::<_, PluginInner>("inner") {
|
||||
res.inner = v;
|
||||
}
|
||||
|
||||
if let Ok(v) = tab.get::<_, Function>("on_init") {
|
||||
res.on_init = Some(v);
|
||||
}
|
||||
|
||||
if let Ok(v) = tab.get::<_, PluginBuildInfo>("build") {
|
||||
res.build = v;
|
||||
}
|
||||
|
||||
if let Ok(v) = tab.get::<_, PluginServeInfo>("serve") {
|
||||
res.serve = v;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'lua> ToLua<'lua> for PluginInfo<'lua> {
|
||||
fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result<mlua::Value<'lua>> {
|
||||
let res = lua.create_table()?;
|
||||
|
||||
res.set("name", self.name.to_string())?;
|
||||
res.set("repository", self.repository.to_string())?;
|
||||
res.set("author", self.author.to_string())?;
|
||||
res.set("version", self.version.to_string())?;
|
||||
|
||||
res.set("inner", self.inner)?;
|
||||
|
||||
if let Some(e) = self.on_init {
|
||||
res.set("on_init", e)?;
|
||||
}
|
||||
res.set("build", self.build)?;
|
||||
res.set("serve", self.serve)?;
|
||||
|
||||
Ok(mlua::Value::Table(res))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PluginInner {
|
||||
pub plugin_dir: String,
|
||||
pub from_loader: bool,
|
||||
}
|
||||
|
||||
impl<'lua> FromLua<'lua> for PluginInner {
|
||||
fn from_lua(lua_value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result<Self> {
|
||||
let mut res = Self {
|
||||
plugin_dir: String::new(),
|
||||
from_loader: false,
|
||||
};
|
||||
|
||||
if let mlua::Value::Table(t) = lua_value {
|
||||
if let Ok(v) = t.get::<_, String>("plugin_dir") {
|
||||
res.plugin_dir = v;
|
||||
}
|
||||
if let Ok(v) = t.get::<_, bool>("from_loader") {
|
||||
res.from_loader = v;
|
||||
}
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'lua> ToLua<'lua> for PluginInner {
|
||||
fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result<mlua::Value<'lua>> {
|
||||
let res = lua.create_table()?;
|
||||
|
||||
res.set("plugin_dir", self.plugin_dir)?;
|
||||
res.set("from_loader", self.from_loader)?;
|
||||
|
||||
Ok(mlua::Value::Table(res))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PluginBuildInfo<'lua> {
|
||||
pub on_start: Option<Function<'lua>>,
|
||||
pub on_finish: Option<Function<'lua>>,
|
||||
}
|
||||
|
||||
impl<'lua> FromLua<'lua> for PluginBuildInfo<'lua> {
|
||||
fn from_lua(lua_value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result<Self> {
|
||||
let mut res = Self {
|
||||
on_start: None,
|
||||
on_finish: None,
|
||||
};
|
||||
|
||||
if let mlua::Value::Table(t) = lua_value {
|
||||
if let Ok(v) = t.get::<_, Function>("on_start") {
|
||||
res.on_start = Some(v);
|
||||
}
|
||||
if let Ok(v) = t.get::<_, Function>("on_finish") {
|
||||
res.on_finish = Some(v);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'lua> ToLua<'lua> for PluginBuildInfo<'lua> {
|
||||
fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result<mlua::Value<'lua>> {
|
||||
let res = lua.create_table()?;
|
||||
|
||||
if let Some(v) = self.on_start {
|
||||
res.set("on_start", v)?;
|
||||
}
|
||||
|
||||
if let Some(v) = self.on_finish {
|
||||
res.set("on_finish", v)?;
|
||||
}
|
||||
|
||||
Ok(mlua::Value::Table(res))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PluginServeInfo<'lua> {
|
||||
pub interval: i32,
|
||||
|
||||
pub on_start: Option<Function<'lua>>,
|
||||
pub on_interval: Option<Function<'lua>>,
|
||||
pub on_rebuild: Option<Function<'lua>>,
|
||||
pub on_shutdown: Option<Function<'lua>>,
|
||||
}
|
||||
|
||||
impl<'lua> FromLua<'lua> for PluginServeInfo<'lua> {
|
||||
fn from_lua(lua_value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result<Self> {
|
||||
let mut res = Self::default();
|
||||
|
||||
if let mlua::Value::Table(tab) = lua_value {
|
||||
if let Ok(v) = tab.get::<_, i32>("interval") {
|
||||
res.interval = v;
|
||||
}
|
||||
if let Ok(v) = tab.get::<_, Function>("on_start") {
|
||||
res.on_start = Some(v);
|
||||
}
|
||||
if let Ok(v) = tab.get::<_, Function>("on_interval") {
|
||||
res.on_interval = Some(v);
|
||||
}
|
||||
if let Ok(v) = tab.get::<_, Function>("on_rebuild") {
|
||||
res.on_rebuild = Some(v);
|
||||
}
|
||||
if let Ok(v) = tab.get::<_, Function>("on_shutdown") {
|
||||
res.on_shutdown = Some(v);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'lua> ToLua<'lua> for PluginServeInfo<'lua> {
|
||||
fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result<mlua::Value<'lua>> {
|
||||
let res = lua.create_table()?;
|
||||
|
||||
res.set("interval", self.interval)?;
|
||||
|
||||
if let Some(v) = self.on_start {
|
||||
res.set("on_start", v)?;
|
||||
}
|
||||
|
||||
if let Some(v) = self.on_interval {
|
||||
res.set("on_interval", v)?;
|
||||
}
|
||||
|
||||
if let Some(v) = self.on_rebuild {
|
||||
res.set("on_rebuild", v)?;
|
||||
}
|
||||
|
||||
if let Some(v) = self.on_shutdown {
|
||||
res.set("on_shutdown", v)?;
|
||||
}
|
||||
|
||||
Ok(mlua::Value::Table(res))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
use std::{io::Cursor, path::PathBuf};
|
||||
|
||||
use mlua::UserData;
|
||||
|
||||
pub struct PluginNetwork;
|
||||
impl UserData for PluginNetwork {
|
||||
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_function("download_file", |_, args: (String, String)| {
|
||||
let url = args.0;
|
||||
let path = args.1;
|
||||
|
||||
let resp = reqwest::blocking::get(url);
|
||||
if let Ok(resp) = resp {
|
||||
let mut content = Cursor::new(resp.bytes().unwrap());
|
||||
let file = std::fs::File::create(PathBuf::from(path));
|
||||
if file.is_err() {
|
||||
return Ok(false);
|
||||
}
|
||||
let mut file = file.unwrap();
|
||||
let res = std::io::copy(&mut content, &mut file);
|
||||
return Ok(res.is_ok());
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
use mlua::UserData;
|
||||
|
||||
pub struct PluginOS;
|
||||
impl UserData for PluginOS {
|
||||
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_function("current_platform", |_, ()| {
|
||||
if cfg!(target_os = "windows") {
|
||||
Ok("windows")
|
||||
} else if cfg!(target_os = "macos") {
|
||||
Ok("macos")
|
||||
} else if cfg!(target_os = "linux") {
|
||||
Ok("linux")
|
||||
} else {
|
||||
panic!("unsupported platformm");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use mlua::{UserData, Variadic};
|
||||
|
||||
pub struct PluginPath;
|
||||
impl UserData for PluginPath {
|
||||
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
// join function
|
||||
methods.add_function("join", |_, args: Variadic<String>| {
|
||||
let mut path = PathBuf::new();
|
||||
for i in args {
|
||||
path = path.join(i);
|
||||
}
|
||||
Ok(path.to_str().unwrap().to_string())
|
||||
});
|
||||
|
||||
// parent function
|
||||
methods.add_function("parent", |_, path: String| {
|
||||
let current_path = PathBuf::from(&path);
|
||||
let parent = current_path.parent();
|
||||
|
||||
if let Some(parent) = parent {
|
||||
Ok(parent.to_str().unwrap().to_string())
|
||||
} else {
|
||||
Ok(path)
|
||||
}
|
||||
});
|
||||
methods.add_function("exists", |_, path: String| {
|
||||
let path = PathBuf::from(path);
|
||||
Ok(path.exists())
|
||||
});
|
||||
methods.add_function("is_dir", |_, path: String| {
|
||||
let path = PathBuf::from(path);
|
||||
Ok(path.is_dir())
|
||||
});
|
||||
methods.add_function("is_file", |_, path: String| {
|
||||
let path = PathBuf::from(path);
|
||||
Ok(path.is_file())
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
path::PathBuf,
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
use mlua::{Lua, Table};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
tools::{app_path, clone_repo},
|
||||
CrateConfig,
|
||||
};
|
||||
|
||||
use self::{
|
||||
interface::{
|
||||
command::PluginCommander, dirs::PluginDirs, fs::PluginFileSystem, log::PluginLogger,
|
||||
network::PluginNetwork, os::PluginOS, path::PluginPath, PluginInfo,
|
||||
},
|
||||
types::PluginConfig,
|
||||
};
|
||||
|
||||
pub mod interface;
|
||||
mod types;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref LUA: Mutex<Lua> = Mutex::new(Lua::new());
|
||||
}
|
||||
|
||||
pub struct PluginManager;
|
||||
|
||||
impl PluginManager {
|
||||
pub fn init(config: toml::Value) -> anyhow::Result<()> {
|
||||
let config = PluginConfig::from_toml_value(config);
|
||||
|
||||
if !config.available {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let lua = LUA.lock().unwrap();
|
||||
|
||||
let manager = lua.create_table().unwrap();
|
||||
let name_index = lua.create_table().unwrap();
|
||||
|
||||
let plugin_dir = Self::init_plugin_dir();
|
||||
|
||||
let api = lua.create_table().unwrap();
|
||||
|
||||
api.set("log", PluginLogger).unwrap();
|
||||
api.set("command", PluginCommander).unwrap();
|
||||
api.set("network", PluginNetwork).unwrap();
|
||||
api.set("dirs", PluginDirs).unwrap();
|
||||
api.set("fs", PluginFileSystem).unwrap();
|
||||
api.set("path", PluginPath).unwrap();
|
||||
api.set("os", PluginOS).unwrap();
|
||||
|
||||
lua.globals().set("plugin_lib", api).unwrap();
|
||||
lua.globals()
|
||||
.set("library_dir", plugin_dir.to_str().unwrap())
|
||||
.unwrap();
|
||||
lua.globals().set("config_info", config.clone())?;
|
||||
|
||||
let mut index: u32 = 1;
|
||||
let dirs = std::fs::read_dir(&plugin_dir)?;
|
||||
|
||||
let mut path_list = dirs
|
||||
.filter(|v| v.is_ok())
|
||||
.map(|v| (v.unwrap().path(), false))
|
||||
.collect::<Vec<(PathBuf, bool)>>();
|
||||
for i in &config.loader {
|
||||
let path = PathBuf::from(i);
|
||||
if !path.is_dir() {
|
||||
// for loader dir, we need check first, because we need give a error log.
|
||||
log::error!("Plugin loader: {:?} path is not a exists directory.", path);
|
||||
}
|
||||
path_list.push((path, true));
|
||||
}
|
||||
|
||||
for entry in path_list {
|
||||
let plugin_dir = entry.0.to_path_buf();
|
||||
|
||||
if plugin_dir.is_dir() {
|
||||
let init_file = plugin_dir.join("init.lua");
|
||||
if init_file.is_file() {
|
||||
let mut file = std::fs::File::open(init_file).unwrap();
|
||||
let mut buffer = String::new();
|
||||
file.read_to_string(&mut buffer).unwrap();
|
||||
|
||||
let current_plugin_dir = plugin_dir.to_str().unwrap().to_string();
|
||||
let from_loader = entry.1;
|
||||
|
||||
lua.globals()
|
||||
.set("_temp_plugin_dir", current_plugin_dir.clone())?;
|
||||
lua.globals().set("_temp_from_loader", from_loader)?;
|
||||
|
||||
let info = lua.load(&buffer).eval::<PluginInfo>();
|
||||
match info {
|
||||
Ok(mut info) => {
|
||||
if name_index.contains_key(info.name.clone()).unwrap_or(false)
|
||||
&& !from_loader
|
||||
{
|
||||
// found same name plugin, intercept load
|
||||
log::warn!(
|
||||
"Plugin {} has been intercepted. [mulit-load]",
|
||||
info.name
|
||||
);
|
||||
continue;
|
||||
}
|
||||
info.inner.plugin_dir = current_plugin_dir;
|
||||
info.inner.from_loader = from_loader;
|
||||
|
||||
// call `on_init` if file "dcp.json" not exists
|
||||
let dcp_file = plugin_dir.join("dcp.json");
|
||||
if !dcp_file.is_file() {
|
||||
if let Some(func) = info.clone().on_init {
|
||||
let result = func.call::<_, bool>(());
|
||||
match result {
|
||||
Ok(true) => {
|
||||
// plugin init success, create `dcp.json` file.
|
||||
let mut file = std::fs::File::create(dcp_file).unwrap();
|
||||
let value = json!({
|
||||
"name": info.name,
|
||||
"author": info.author,
|
||||
"repository": info.repository,
|
||||
"version": info.version,
|
||||
"generate_time": chrono::Local::now().timestamp(),
|
||||
});
|
||||
let buffer =
|
||||
serde_json::to_string_pretty(&value).unwrap();
|
||||
let buffer = buffer.as_bytes();
|
||||
file.write_all(buffer).unwrap();
|
||||
|
||||
// insert plugin-info into plugin-manager
|
||||
if let Ok(index) =
|
||||
name_index.get::<_, u32>(info.name.clone())
|
||||
{
|
||||
let _ = manager.set(index, info.clone());
|
||||
} else {
|
||||
let _ = manager.set(index, info.clone());
|
||||
index += 1;
|
||||
let _ = name_index.set(info.name, index);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log::warn!(
|
||||
"Plugin init function result is `false`, init failed."
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Plugin init failed: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Ok(index) = name_index.get::<_, u32>(info.name.clone()) {
|
||||
let _ = manager.set(index, info.clone());
|
||||
} else {
|
||||
let _ = manager.set(index, info.clone());
|
||||
index += 1;
|
||||
let _ = name_index.set(info.name, index);
|
||||
}
|
||||
}
|
||||
Err(_e) => {
|
||||
let dir_name = plugin_dir.file_name().unwrap().to_str().unwrap();
|
||||
log::error!("Plugin '{dir_name}' load failed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lua.globals().set("manager", manager).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn on_build_start(crate_config: &CrateConfig, platform: &str) -> anyhow::Result<()> {
|
||||
let lua = LUA.lock().unwrap();
|
||||
|
||||
if !lua.globals().contains_key("manager")? {
|
||||
return Ok(());
|
||||
}
|
||||
let manager = lua.globals().get::<_, Table>("manager")?;
|
||||
|
||||
let args = lua.create_table()?;
|
||||
args.set("name", crate_config.dioxus_config.application.name.clone())?;
|
||||
args.set("platform", platform)?;
|
||||
args.set("out_dir", crate_config.out_dir.to_str().unwrap())?;
|
||||
args.set("asset_dir", crate_config.asset_dir.to_str().unwrap())?;
|
||||
|
||||
for i in 1..(manager.len()? as i32 + 1) {
|
||||
let info = manager.get::<i32, PluginInfo>(i)?;
|
||||
if let Some(func) = info.build.on_start {
|
||||
func.call::<Table, ()>(args.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn on_build_finish(crate_config: &CrateConfig, platform: &str) -> anyhow::Result<()> {
|
||||
let lua = LUA.lock().unwrap();
|
||||
|
||||
if !lua.globals().contains_key("manager")? {
|
||||
return Ok(());
|
||||
}
|
||||
let manager = lua.globals().get::<_, Table>("manager")?;
|
||||
|
||||
let args = lua.create_table()?;
|
||||
args.set("name", crate_config.dioxus_config.application.name.clone())?;
|
||||
args.set("platform", platform)?;
|
||||
args.set("out_dir", crate_config.out_dir.to_str().unwrap())?;
|
||||
args.set("asset_dir", crate_config.asset_dir.to_str().unwrap())?;
|
||||
|
||||
for i in 1..(manager.len()? as i32 + 1) {
|
||||
let info = manager.get::<i32, PluginInfo>(i)?;
|
||||
if let Some(func) = info.build.on_finish {
|
||||
func.call::<Table, ()>(args.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn on_serve_start(crate_config: &CrateConfig) -> anyhow::Result<()> {
|
||||
let lua = LUA.lock().unwrap();
|
||||
|
||||
if !lua.globals().contains_key("manager")? {
|
||||
return Ok(());
|
||||
}
|
||||
let manager = lua.globals().get::<_, Table>("manager")?;
|
||||
|
||||
let args = lua.create_table()?;
|
||||
args.set("name", crate_config.dioxus_config.application.name.clone())?;
|
||||
|
||||
for i in 1..(manager.len()? as i32 + 1) {
|
||||
let info = manager.get::<i32, PluginInfo>(i)?;
|
||||
if let Some(func) = info.serve.on_start {
|
||||
func.call::<Table, ()>(args.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn on_serve_rebuild(timestamp: i64, files: Vec<PathBuf>) -> anyhow::Result<()> {
|
||||
let lua = LUA.lock().unwrap();
|
||||
|
||||
let manager = lua.globals().get::<_, Table>("manager")?;
|
||||
|
||||
let args = lua.create_table()?;
|
||||
args.set("timestamp", timestamp)?;
|
||||
let files: Vec<String> = files
|
||||
.iter()
|
||||
.map(|v| v.to_str().unwrap().to_string())
|
||||
.collect();
|
||||
args.set("changed_files", files)?;
|
||||
|
||||
for i in 1..(manager.len()? as i32 + 1) {
|
||||
let info = manager.get::<i32, PluginInfo>(i)?;
|
||||
if let Some(func) = info.serve.on_rebuild {
|
||||
func.call::<Table, ()>(args.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn on_serve_shutdown(crate_config: &CrateConfig) -> anyhow::Result<()> {
|
||||
let lua = LUA.lock().unwrap();
|
||||
|
||||
if !lua.globals().contains_key("manager")? {
|
||||
return Ok(());
|
||||
}
|
||||
let manager = lua.globals().get::<_, Table>("manager")?;
|
||||
|
||||
let args = lua.create_table()?;
|
||||
args.set("name", crate_config.dioxus_config.application.name.clone())?;
|
||||
|
||||
for i in 1..(manager.len()? as i32 + 1) {
|
||||
let info = manager.get::<i32, PluginInfo>(i)?;
|
||||
if let Some(func) = info.serve.on_shutdown {
|
||||
func.call::<Table, ()>(args.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn init_plugin_dir() -> PathBuf {
|
||||
let app_path = app_path();
|
||||
let plugin_path = app_path.join("plugins");
|
||||
if !plugin_path.is_dir() {
|
||||
log::info!("📖 Start to init plugin library ...");
|
||||
let url = "https://github.com/DioxusLabs/cli-plugin-library";
|
||||
if let Err(err) = clone_repo(&plugin_path, url) {
|
||||
log::error!("Failed to init plugin dir, error caused by {}. ", err);
|
||||
}
|
||||
}
|
||||
plugin_path
|
||||
}
|
||||
|
||||
pub fn plugin_list() -> Vec<String> {
|
||||
let mut res = vec![];
|
||||
|
||||
if let Ok(lua) = LUA.lock() {
|
||||
let list = lua
|
||||
.load(mlua::chunk!(
|
||||
local list = {}
|
||||
for key, value in ipairs(manager) do
|
||||
table.insert(list, {name = value.name, loader = value.inner.from_loader})
|
||||
end
|
||||
return list
|
||||
))
|
||||
.eval::<Vec<Table>>()
|
||||
.unwrap_or_default();
|
||||
for i in list {
|
||||
let name = i.get::<_, String>("name").unwrap();
|
||||
let loader = i.get::<_, bool>("loader").unwrap();
|
||||
|
||||
let text = if loader {
|
||||
format!("{name} [:loader]")
|
||||
} else {
|
||||
name
|
||||
};
|
||||
res.push(text);
|
||||
}
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use mlua::ToLua;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PluginConfig {
|
||||
pub available: bool,
|
||||
pub loader: Vec<String>,
|
||||
pub config_info: HashMap<String, HashMap<String, Value>>,
|
||||
}
|
||||
|
||||
impl<'lua> ToLua<'lua> for PluginConfig {
|
||||
fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result<mlua::Value<'lua>> {
|
||||
let table = lua.create_table()?;
|
||||
|
||||
table.set("available", self.available)?;
|
||||
table.set("loader", self.loader)?;
|
||||
|
||||
let config_info = lua.create_table()?;
|
||||
|
||||
for (name, data) in self.config_info {
|
||||
config_info.set(name, data)?;
|
||||
}
|
||||
|
||||
table.set("config_info", config_info)?;
|
||||
|
||||
Ok(mlua::Value::Table(table))
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginConfig {
|
||||
pub fn from_toml_value(val: toml::Value) -> Self {
|
||||
if let toml::Value::Table(tab) = val {
|
||||
let available = tab
|
||||
.get::<_>("available")
|
||||
.unwrap_or(&toml::Value::Boolean(true));
|
||||
let available = available.as_bool().unwrap_or(true);
|
||||
|
||||
let mut loader = vec![];
|
||||
if let Some(origin) = tab.get("loader") {
|
||||
if origin.is_array() {
|
||||
for i in origin.as_array().unwrap() {
|
||||
loader.push(i.as_str().unwrap_or_default().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut config_info = HashMap::new();
|
||||
|
||||
for (name, value) in tab {
|
||||
if name == "available" || name == "loader" {
|
||||
continue;
|
||||
}
|
||||
if let toml::Value::Table(value) = value {
|
||||
let mut map = HashMap::new();
|
||||
for (item, info) in value {
|
||||
map.insert(item, Value::from_toml(info));
|
||||
}
|
||||
config_info.insert(name, map);
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
available,
|
||||
loader,
|
||||
config_info,
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
available: false,
|
||||
loader: vec![],
|
||||
config_info: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Value {
|
||||
String(String),
|
||||
Integer(i64),
|
||||
Float(f64),
|
||||
Boolean(bool),
|
||||
Array(Vec<Value>),
|
||||
Table(HashMap<String, Value>),
|
||||
}
|
||||
|
||||
impl Value {
|
||||
pub fn from_toml(origin: toml::Value) -> Self {
|
||||
match origin {
|
||||
cargo_toml::Value::String(s) => Value::String(s),
|
||||
cargo_toml::Value::Integer(i) => Value::Integer(i),
|
||||
cargo_toml::Value::Float(f) => Value::Float(f),
|
||||
cargo_toml::Value::Boolean(b) => Value::Boolean(b),
|
||||
cargo_toml::Value::Datetime(d) => Value::String(d.to_string()),
|
||||
cargo_toml::Value::Array(a) => {
|
||||
let mut v = vec![];
|
||||
for i in a {
|
||||
v.push(Value::from_toml(i));
|
||||
}
|
||||
Value::Array(v)
|
||||
}
|
||||
cargo_toml::Value::Table(t) => {
|
||||
let mut h = HashMap::new();
|
||||
for (n, v) in t {
|
||||
h.insert(n, Value::from_toml(v));
|
||||
}
|
||||
Value::Table(h)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'lua> ToLua<'lua> for Value {
|
||||
fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result<mlua::Value<'lua>> {
|
||||
Ok(match self {
|
||||
Value::String(s) => mlua::Value::String(lua.create_string(&s)?),
|
||||
Value::Integer(i) => mlua::Value::Integer(i),
|
||||
Value::Float(f) => mlua::Value::Number(f),
|
||||
Value::Boolean(b) => mlua::Value::Boolean(b),
|
||||
Value::Array(a) => {
|
||||
let table = lua.create_table()?;
|
||||
for (i, v) in a.iter().enumerate() {
|
||||
table.set(i, v.clone())?;
|
||||
}
|
||||
mlua::Value::Table(table)
|
||||
}
|
||||
Value::Table(t) => {
|
||||
let table = lua.create_table()?;
|
||||
for (i, v) in t.iter() {
|
||||
table.set(i.clone(), v.clone())?;
|
||||
}
|
||||
mlua::Value::Table(table)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use axum::{
|
||||
extract::{ws::Message, WebSocketUpgrade},
|
||||
response::IntoResponse,
|
||||
Extension, TypedHeader,
|
||||
};
|
||||
use dioxus_core::Template;
|
||||
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,
|
||||
}
|
||||
|
||||
pub async fn hot_reload_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
_: Option<TypedHeader<headers::UserAgent>>,
|
||||
Extension(state): Extension<Arc<HotReloadState>>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(|mut socket| async move {
|
||||
log::info!("🔥 Hot Reload WebSocket connected");
|
||||
{
|
||||
// update any rsx calls that changed before the websocket connected.
|
||||
{
|
||||
log::info!("🔮 Finding updates since last compile...");
|
||||
let templates: Vec<_> = {
|
||||
state
|
||||
.file_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.map
|
||||
.values()
|
||||
.filter_map(|(_, template_slot)| *template_slot)
|
||||
.collect()
|
||||
};
|
||||
for template in templates {
|
||||
if socket
|
||||
.send(Message::Text(serde_json::to_string(&template).unwrap()))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
log::info!("finished");
|
||||
}
|
||||
|
||||
let mut rx = state.messages.subscribe();
|
||||
loop {
|
||||
if let Ok(rsx) = rx.recv().await {
|
||||
if socket
|
||||
.send(Message::Text(serde_json::to_string(&rsx).unwrap()))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,655 @@
|
||||
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 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::*;
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
/// Sets up a file watcher
|
||||
async fn setup_file_watcher(
|
||||
config: &CrateConfig,
|
||||
port: u16,
|
||||
watcher_ip: String,
|
||||
reload_tx: Sender<()>,
|
||||
) -> 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
|
||||
let allow_watch_path = config
|
||||
.dioxus_config
|
||||
.web
|
||||
.watcher
|
||||
.watch_path
|
||||
.clone()
|
||||
.unwrap_or_else(|| vec![PathBuf::from("src")]);
|
||||
|
||||
let watcher_config = config.clone();
|
||||
let mut watcher = notify::recommended_watcher(move |info: notify::Result<notify::Event>| {
|
||||
let config = watcher_config.clone();
|
||||
if let Ok(e) = info {
|
||||
if chrono::Local::now().timestamp() > last_update_time {
|
||||
match build_manager.rebuild() {
|
||||
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,
|
||||
},
|
||||
);
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
let _ = PluginManager::on_serve_rebuild(
|
||||
chrono::Local::now().timestamp(),
|
||||
e.paths,
|
||||
);
|
||||
}
|
||||
Err(e) => log::error!("{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
for sub_path in allow_watch_path {
|
||||
watcher
|
||||
.watch(
|
||||
&config.crate_dir.join(sub_path),
|
||||
notify::RecursiveMode::Recursive,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
Ok(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(
|
||||
config: &CrateConfig,
|
||||
port: u16,
|
||||
watcher_ip: String,
|
||||
hot_reload_tx: Sender<Template<'static>>,
|
||||
file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
|
||||
build_manager: Arc<BuildManager>,
|
||||
) -> Result<RecommendedWatcher> {
|
||||
// file watcher: check file change
|
||||
let allow_watch_path = config
|
||||
.dioxus_config
|
||||
.web
|
||||
.watcher
|
||||
.watch_path
|
||||
.clone()
|
||||
.unwrap_or_else(|| vec![PathBuf::from("src")]);
|
||||
|
||||
let watcher_config = config.clone();
|
||||
let mut last_update_time = chrono::Local::now().timestamp();
|
||||
|
||||
let mut watcher = RecommendedWatcher::new(
|
||||
move |evt: notify::Result<notify::Event>| {
|
||||
let config = watcher_config.clone();
|
||||
// Give time for the change to take effect before reading the file
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
if chrono::Local::now().timestamp() > last_update_time {
|
||||
if let Ok(evt) = evt {
|
||||
let mut messages: Vec<Template<'static>> = Vec::new();
|
||||
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() {
|
||||
Ok(res) => {
|
||||
print_console_info(
|
||||
&watcher_ip,
|
||||
port,
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: evt.paths,
|
||||
warnings: res.warnings,
|
||||
elapsed_time: res.elapsed_time,
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// find changes to the rsx in the file
|
||||
let mut map = file_map.lock().unwrap();
|
||||
|
||||
match map.update_rsx(&path, &config.crate_dir) {
|
||||
Ok(UpdateResult::UpdatedRsx(msgs)) => {
|
||||
messages.extend(msgs);
|
||||
}
|
||||
Ok(UpdateResult::NeedsRebuild) => {
|
||||
match build_manager.rebuild() {
|
||||
Ok(res) => {
|
||||
print_console_info(
|
||||
&watcher_ip,
|
||||
port,
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: evt.paths,
|
||||
warnings: res.warnings,
|
||||
elapsed_time: res.elapsed_time,
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
for msg in messages {
|
||||
let _ = hot_reload_tx.send(msg);
|
||||
}
|
||||
}
|
||||
last_update_time = chrono::Local::now().timestamp();
|
||||
}
|
||||
},
|
||||
notify::Config::default(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for sub_path in allow_watch_path {
|
||||
if let Err(err) = watcher.watch(
|
||||
&config.crate_dir.join(&sub_path),
|
||||
notify::RecursiveMode::Recursive,
|
||||
) {
|
||||
log::error!("error watching {sub_path:?}: \n{}", err);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
use crate::server::Diagnostic;
|
||||
use crate::CrateConfig;
|
||||
use colored::Colorize;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PrettierOptions {
|
||||
pub changed: Vec<PathBuf>,
|
||||
pub warnings: Vec<Diagnostic>,
|
||||
pub elapsed_time: u128,
|
||||
}
|
||||
|
||||
pub fn print_console_info(ip: &String, port: u16, config: &CrateConfig, options: PrettierOptions) {
|
||||
if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") {
|
||||
"cls"
|
||||
} else {
|
||||
"clear"
|
||||
})
|
||||
.output()
|
||||
{
|
||||
print!("{}", String::from_utf8_lossy(&native_clearseq.stdout));
|
||||
} else {
|
||||
// Try ANSI-Escape characters
|
||||
print!("\x1b[2J\x1b[H");
|
||||
}
|
||||
|
||||
let mut profile = if config.release { "Release" } else { "Debug" }.to_string();
|
||||
if config.custom_profile.is_some() {
|
||||
profile = config.custom_profile.as_ref().unwrap().to_string();
|
||||
}
|
||||
let hot_reload = if config.hot_reload { "RSX" } else { "Normal" };
|
||||
let crate_root = crate::cargo::crate_root().unwrap();
|
||||
let custom_html_file = if crate_root.join("index.html").is_file() {
|
||||
"Custom [index.html]"
|
||||
} else {
|
||||
"Default"
|
||||
};
|
||||
let url_rewrite = if config
|
||||
.dioxus_config
|
||||
.web
|
||||
.watcher
|
||||
.index_on_404
|
||||
.unwrap_or(false)
|
||||
{
|
||||
"True"
|
||||
} else {
|
||||
"False"
|
||||
};
|
||||
|
||||
let proxies = config.dioxus_config.web.proxy.as_ref();
|
||||
|
||||
if options.changed.is_empty() {
|
||||
println!(
|
||||
"{} @ v{} [{}] \n",
|
||||
"Dioxus".bold().green(),
|
||||
crate::DIOXUS_CLI_VERSION,
|
||||
chrono::Local::now().format("%H:%M:%S").to_string().dimmed()
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"Project Reloaded: {}\n",
|
||||
format!(
|
||||
"Changed {} files. [{}]",
|
||||
options.changed.len(),
|
||||
chrono::Local::now().format("%H:%M:%S").to_string().dimmed()
|
||||
)
|
||||
.purple()
|
||||
.bold()
|
||||
);
|
||||
}
|
||||
|
||||
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());
|
||||
println!("\t> Hot Reload : {}", hot_reload.cyan());
|
||||
if let Some(proxies) = proxies {
|
||||
if !proxies.is_empty() {
|
||||
println!("\t> Proxies :");
|
||||
for proxy in proxies {
|
||||
println!("\t\t- {}", proxy.backend.blue());
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("\t> Index Template : {}", custom_html_file.green());
|
||||
println!("\t> URL Rewrite [index_on_404] : {}", url_rewrite.purple());
|
||||
println!();
|
||||
println!(
|
||||
"\t> Build Time Use : {} millis",
|
||||
options.elapsed_time.to_string().green().bold()
|
||||
);
|
||||
println!();
|
||||
|
||||
if options.warnings.is_empty() {
|
||||
log::info!("{}\n", "A perfect compilation!".green().bold());
|
||||
} else {
|
||||
log::warn!(
|
||||
"{}",
|
||||
format!(
|
||||
"There were {} warning messages during the build.",
|
||||
options.warnings.len() - 1
|
||||
)
|
||||
.yellow()
|
||||
.bold()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
use crate::{Result, WebProxyConfig};
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{http::StatusCode, routing::any, Router};
|
||||
use hyper::{Request, Response, Uri};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ProxyClient {
|
||||
inner: hyper::Client<hyper_rustls::HttpsConnector<hyper::client::HttpConnector>>,
|
||||
url: Uri,
|
||||
}
|
||||
|
||||
impl ProxyClient {
|
||||
fn new(url: Uri) -> Self {
|
||||
let https = hyper_rustls::HttpsConnectorBuilder::new()
|
||||
.with_native_roots()
|
||||
.https_or_http()
|
||||
.enable_http1()
|
||||
.build();
|
||||
Self {
|
||||
inner: hyper::Client::builder().build(https),
|
||||
url,
|
||||
}
|
||||
}
|
||||
|
||||
async fn send(
|
||||
&self,
|
||||
mut req: Request<hyper::body::Body>,
|
||||
) -> Result<Response<hyper::body::Body>> {
|
||||
let mut uri_parts = req.uri().clone().into_parts();
|
||||
uri_parts.authority = self.url.authority().cloned();
|
||||
uri_parts.scheme = self.url.scheme().cloned();
|
||||
*req.uri_mut() = Uri::from_parts(uri_parts).context("Invalid URI parts")?;
|
||||
self.inner
|
||||
.request(req)
|
||||
.await
|
||||
.map_err(crate::error::Error::ProxyRequestError)
|
||||
}
|
||||
}
|
||||
|
||||
/// Add routes to the router handling the specified proxy config.
|
||||
///
|
||||
/// We will proxy requests directed at either:
|
||||
///
|
||||
/// - the exact path of the proxy config's backend URL, e.g. /api
|
||||
/// - the exact path with a trailing slash, e.g. /api/
|
||||
/// - any subpath of the backend URL, e.g. /api/foo/bar
|
||||
pub fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result<Router> {
|
||||
let url: Uri = proxy.backend.parse()?;
|
||||
let path = url.path().to_string();
|
||||
let client = ProxyClient::new(url);
|
||||
|
||||
// We also match everything after the path using a wildcard matcher.
|
||||
let wildcard_client = client.clone();
|
||||
|
||||
router = router.route(
|
||||
// Always remove trailing /'s so that the exact route
|
||||
// matches.
|
||||
path.trim_end_matches('/'),
|
||||
any(move |req| async move {
|
||||
client
|
||||
.send(req)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
|
||||
}),
|
||||
);
|
||||
|
||||
// Wildcard match anything else _after_ the backend URL's path.
|
||||
// Note that we know `path` ends with a trailing `/` in this branch,
|
||||
// so `wildcard` will look like `http://localhost/api/*proxywildcard`.
|
||||
let wildcard = format!("{}/*proxywildcard", path.trim_end_matches('/'));
|
||||
router = router.route(
|
||||
&wildcard,
|
||||
any(move |req| async move {
|
||||
wildcard_client
|
||||
.send(req)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
|
||||
}),
|
||||
);
|
||||
Ok(router)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use super::*;
|
||||
|
||||
use axum::{extract::Path, Router};
|
||||
|
||||
fn setup_servers(
|
||||
mut config: WebProxyConfig,
|
||||
) -> (
|
||||
tokio::task::JoinHandle<()>,
|
||||
tokio::task::JoinHandle<()>,
|
||||
String,
|
||||
) {
|
||||
let backend_router = Router::new().route(
|
||||
"/*path",
|
||||
any(|path: Path<String>| async move { format!("backend: {}", path.0) }),
|
||||
);
|
||||
let backend_server = axum::Server::bind(&"127.0.0.1:0".parse().unwrap())
|
||||
.serve(backend_router.into_make_service());
|
||||
let backend_addr = backend_server.local_addr();
|
||||
let backend_handle = tokio::spawn(async move { backend_server.await.unwrap() });
|
||||
config.backend = format!("http://{}{}", backend_addr, config.backend);
|
||||
let router = super::add_proxy(Router::new(), &config);
|
||||
let server = axum::Server::bind(&"127.0.0.1:0".parse().unwrap())
|
||||
.serve(router.unwrap().into_make_service());
|
||||
let server_addr = server.local_addr();
|
||||
let server_handle = tokio::spawn(async move { server.await.unwrap() });
|
||||
(backend_handle, server_handle, server_addr.to_string())
|
||||
}
|
||||
|
||||
async fn test_proxy_requests(path: String) {
|
||||
let config = WebProxyConfig {
|
||||
// Normally this would be an absolute URL including scheme/host/port,
|
||||
// but in these tests we need to let the OS choose the port so tests
|
||||
// don't conflict, so we'll concatenate the final address and this
|
||||
// path together.
|
||||
// So in day to day usage, use `http://localhost:8000/api` instead!
|
||||
backend: path,
|
||||
};
|
||||
let (backend_handle, server_handle, server_addr) = setup_servers(config);
|
||||
let resp = hyper::Client::new()
|
||||
.get(format!("http://{}/api", server_addr).parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
hyper::body::to_bytes(resp.into_body()).await.unwrap(),
|
||||
"backend: /api"
|
||||
);
|
||||
|
||||
let resp = hyper::Client::new()
|
||||
.get(format!("http://{}/api/", server_addr).parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
hyper::body::to_bytes(resp.into_body()).await.unwrap(),
|
||||
"backend: /api/"
|
||||
);
|
||||
|
||||
let resp = hyper::Client::new()
|
||||
.get(
|
||||
format!("http://{}/api/subpath", server_addr)
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
hyper::body::to_bytes(resp.into_body()).await.unwrap(),
|
||||
"backend: /api/subpath"
|
||||
);
|
||||
backend_handle.abort();
|
||||
server_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_proxy() {
|
||||
test_proxy_requests("/api".to_string()).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_proxy_trailing_slash() {
|
||||
test_proxy_requests("/api/".to_string()).await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
use std::{
|
||||
fs::{create_dir_all, File},
|
||||
io::{ErrorKind, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use flate2::read::GzDecoder;
|
||||
use futures::StreamExt;
|
||||
use tar::Archive;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Tool {
|
||||
Binaryen,
|
||||
Sass,
|
||||
Tailwind,
|
||||
}
|
||||
|
||||
// pub fn tool_list() -> Vec<&'static str> {
|
||||
// vec!["binaryen", "sass", "tailwindcss"]
|
||||
// }
|
||||
|
||||
pub fn app_path() -> PathBuf {
|
||||
let data_local = dirs::data_local_dir().unwrap();
|
||||
let dioxus_dir = data_local.join("dioxus");
|
||||
if !dioxus_dir.is_dir() {
|
||||
create_dir_all(&dioxus_dir).unwrap();
|
||||
}
|
||||
dioxus_dir
|
||||
}
|
||||
|
||||
pub fn temp_path() -> PathBuf {
|
||||
let app_path = app_path();
|
||||
let temp_path = app_path.join("temp");
|
||||
if !temp_path.is_dir() {
|
||||
create_dir_all(&temp_path).unwrap();
|
||||
}
|
||||
temp_path
|
||||
}
|
||||
|
||||
pub fn clone_repo(dir: &Path, url: &str) -> anyhow::Result<()> {
|
||||
let target_dir = dir.parent().unwrap();
|
||||
let dir_name = dir.file_name().unwrap();
|
||||
|
||||
let mut cmd = Command::new("git");
|
||||
let cmd = cmd.current_dir(target_dir);
|
||||
let res = cmd.arg("clone").arg(url).arg(dir_name).output();
|
||||
if let Err(err) = res {
|
||||
if ErrorKind::NotFound == err.kind() {
|
||||
log::warn!("Git program not found. Hint: Install git or check $PATH.");
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn tools_path() -> PathBuf {
|
||||
let app_path = app_path();
|
||||
let temp_path = app_path.join("tools");
|
||||
if !temp_path.is_dir() {
|
||||
create_dir_all(&temp_path).unwrap();
|
||||
}
|
||||
temp_path
|
||||
}
|
||||
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
impl Tool {
|
||||
/// from str to tool enum
|
||||
pub fn from_str(name: &str) -> Option<Self> {
|
||||
match name {
|
||||
"binaryen" => Some(Self::Binaryen),
|
||||
"sass" => Some(Self::Sass),
|
||||
"tailwindcss" => Some(Self::Tailwind),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// get current tool name str
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
Self::Binaryen => "binaryen",
|
||||
Self::Sass => "sass",
|
||||
Self::Tailwind => "tailwindcss",
|
||||
}
|
||||
}
|
||||
|
||||
/// get tool bin dir path
|
||||
pub fn bin_path(&self) -> &str {
|
||||
match self {
|
||||
Self::Binaryen => "bin",
|
||||
Self::Sass => ".",
|
||||
Self::Tailwind => ".",
|
||||
}
|
||||
}
|
||||
|
||||
/// get target platform
|
||||
pub fn target_platform(&self) -> &str {
|
||||
match self {
|
||||
Self::Binaryen => {
|
||||
if cfg!(target_os = "windows") {
|
||||
"windows"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"macos"
|
||||
} else if cfg!(target_os = "linux") {
|
||||
"linux"
|
||||
} else {
|
||||
panic!("unsupported platformm");
|
||||
}
|
||||
}
|
||||
Self::Sass => {
|
||||
if cfg!(target_os = "windows") {
|
||||
"windows"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"macos"
|
||||
} else if cfg!(target_os = "linux") {
|
||||
"linux"
|
||||
} else {
|
||||
panic!("unsupported platformm");
|
||||
}
|
||||
}
|
||||
Self::Tailwind => {
|
||||
if cfg!(target_os = "windows") {
|
||||
"windows"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"macos"
|
||||
} else if cfg!(target_os = "linux") {
|
||||
"linux"
|
||||
} else {
|
||||
panic!("unsupported platformm");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// get tool version
|
||||
pub fn tool_version(&self) -> &str {
|
||||
match self {
|
||||
Self::Binaryen => "version_105",
|
||||
Self::Sass => "1.51.0",
|
||||
Self::Tailwind => "v3.1.6",
|
||||
}
|
||||
}
|
||||
|
||||
/// get tool package download url
|
||||
pub fn download_url(&self) -> String {
|
||||
match self {
|
||||
Self::Binaryen => {
|
||||
format!(
|
||||
"https://github.com/WebAssembly/binaryen/releases/download/{version}/binaryen-{version}-x86_64-{target}.tar.gz",
|
||||
version = self.tool_version(),
|
||||
target = self.target_platform()
|
||||
)
|
||||
}
|
||||
Self::Sass => {
|
||||
format!(
|
||||
"https://github.com/sass/dart-sass/releases/download/{version}/dart-sass-{version}-{target}-x64.{extension}",
|
||||
version = self.tool_version(),
|
||||
target = self.target_platform(),
|
||||
extension = self.extension()
|
||||
)
|
||||
}
|
||||
Self::Tailwind => {
|
||||
let windows_extension = match self.target_platform() {
|
||||
"windows" => ".exe",
|
||||
_ => "",
|
||||
};
|
||||
format!(
|
||||
"https://github.com/tailwindlabs/tailwindcss/releases/download/{version}/tailwindcss-{target}-x64{optional_ext}",
|
||||
version = self.tool_version(),
|
||||
target = self.target_platform(),
|
||||
optional_ext = windows_extension
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// get package extension name
|
||||
pub fn extension(&self) -> &str {
|
||||
match self {
|
||||
Self::Binaryen => "tar.gz",
|
||||
Self::Sass => {
|
||||
if cfg!(target_os = "windows") {
|
||||
"zip"
|
||||
} else {
|
||||
"tar.gz"
|
||||
}
|
||||
}
|
||||
Self::Tailwind => "bin",
|
||||
}
|
||||
}
|
||||
|
||||
/// check tool state
|
||||
pub fn is_installed(&self) -> bool {
|
||||
tools_path().join(self.name()).is_dir()
|
||||
}
|
||||
|
||||
/// get download temp path
|
||||
pub fn temp_out_path(&self) -> PathBuf {
|
||||
temp_path().join(format!("{}-tool.tmp", self.name()))
|
||||
}
|
||||
|
||||
/// start to download package
|
||||
pub async fn download_package(&self) -> anyhow::Result<PathBuf> {
|
||||
let download_url = self.download_url();
|
||||
let temp_out = self.temp_out_path();
|
||||
let mut file = tokio::fs::File::create(&temp_out)
|
||||
.await
|
||||
.context("failed creating temporary output file")?;
|
||||
|
||||
let resp = reqwest::get(download_url).await.unwrap();
|
||||
|
||||
let mut res_bytes = resp.bytes_stream();
|
||||
while let Some(chunk_res) = res_bytes.next().await {
|
||||
let chunk = chunk_res.context("error reading chunk from download")?;
|
||||
let _ = file.write(chunk.as_ref()).await;
|
||||
}
|
||||
// log::info!("temp file path: {:?}", temp_out);
|
||||
Ok(temp_out)
|
||||
}
|
||||
|
||||
/// start to install package
|
||||
pub async fn install_package(&self) -> anyhow::Result<()> {
|
||||
let temp_path = self.temp_out_path();
|
||||
let tool_path = tools_path();
|
||||
|
||||
let dir_name = match self {
|
||||
Self::Binaryen => format!("binaryen-{}", self.tool_version()),
|
||||
Self::Sass => "dart-sass".to_string(),
|
||||
Self::Tailwind => self.name().to_string(),
|
||||
};
|
||||
|
||||
if self.extension() == "tar.gz" {
|
||||
let tar_gz = File::open(temp_path)?;
|
||||
let tar = GzDecoder::new(tar_gz);
|
||||
let mut archive = Archive::new(tar);
|
||||
archive.unpack(&tool_path)?;
|
||||
std::fs::rename(tool_path.join(dir_name), tool_path.join(self.name()))?;
|
||||
} else if self.extension() == "zip" {
|
||||
// decompress the `zip` file
|
||||
extract_zip(&temp_path, &tool_path)?;
|
||||
std::fs::rename(tool_path.join(dir_name), tool_path.join(self.name()))?;
|
||||
} else if self.extension() == "bin" {
|
||||
let bin_path = match self.target_platform() {
|
||||
"windows" => tool_path.join(&dir_name).join(self.name()).join(".exe"),
|
||||
_ => tool_path.join(&dir_name).join(self.name()),
|
||||
};
|
||||
// Manualy creating tool directory because we directly download the binary via Github
|
||||
std::fs::create_dir(tool_path.join(dir_name))?;
|
||||
|
||||
let mut final_file = std::fs::File::create(&bin_path)?;
|
||||
let mut temp_file = File::open(&temp_path)?;
|
||||
let mut content = Vec::new();
|
||||
|
||||
temp_file.read_to_end(&mut content)?;
|
||||
final_file.write_all(&content)?;
|
||||
|
||||
if self.target_platform() == "linux" {
|
||||
// This code does not update permissions idk why
|
||||
// let mut perms = final_file.metadata()?.permissions();
|
||||
// perms.set_mode(0o744);
|
||||
|
||||
// Adding to the binary execution rights with "chmod"
|
||||
let mut command = Command::new("chmod");
|
||||
|
||||
let _ = command
|
||||
.args(vec!["+x", bin_path.to_str().unwrap()])
|
||||
.stdout(std::process::Stdio::inherit())
|
||||
.stderr(std::process::Stdio::inherit())
|
||||
.output()?;
|
||||
}
|
||||
|
||||
std::fs::remove_file(&temp_path)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn call(&self, command: &str, args: Vec<&str>) -> anyhow::Result<Vec<u8>> {
|
||||
let bin_path = tools_path().join(self.name()).join(self.bin_path());
|
||||
|
||||
let command_file = match self {
|
||||
Tool::Binaryen => {
|
||||
if cfg!(target_os = "windows") {
|
||||
format!("{}.exe", command)
|
||||
} else {
|
||||
command.to_string()
|
||||
}
|
||||
}
|
||||
Tool::Sass => {
|
||||
if cfg!(target_os = "windows") {
|
||||
format!("{}.bat", command)
|
||||
} else {
|
||||
command.to_string()
|
||||
}
|
||||
}
|
||||
Tool::Tailwind => {
|
||||
if cfg!(target_os = "windows") {
|
||||
format!("{}.exe", command)
|
||||
} else {
|
||||
command.to_string()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !bin_path.join(&command_file).is_file() {
|
||||
return Err(anyhow::anyhow!("Command file not found."));
|
||||
}
|
||||
|
||||
let mut command = Command::new(bin_path.join(&command_file).to_str().unwrap());
|
||||
|
||||
let output = command
|
||||
.args(&args[..])
|
||||
.stdout(std::process::Stdio::inherit())
|
||||
.stderr(std::process::Stdio::inherit())
|
||||
.output()?;
|
||||
Ok(output.stdout)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_zip(file: &Path, target: &Path) -> anyhow::Result<()> {
|
||||
let zip_file = std::fs::File::open(file)?;
|
||||
let mut zip = zip::ZipArchive::new(zip_file)?;
|
||||
|
||||
if !target.exists() {
|
||||
std::fs::create_dir_all(target)?;
|
||||
}
|
||||
|
||||
for i in 0..zip.len() {
|
||||
let mut file = zip.by_index(i)?;
|
||||
if file.is_dir() {
|
||||
// dir
|
||||
let target = target.join(Path::new(&file.name().replace('\\', "")));
|
||||
std::fs::create_dir_all(target)?;
|
||||
} else {
|
||||
// file
|
||||
let file_path = target.join(Path::new(file.name()));
|
||||
let mut target_file = if !file_path.exists() {
|
||||
std::fs::File::create(file_path)?
|
||||
} else {
|
||||
std::fs::File::open(file_path)?
|
||||
};
|
||||
let _num = std::io::copy(&mut file, &mut target_file)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
#[test]
|
||||
fn ready() {
|
||||
println!("Compiled successfully!")
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<div>
|
||||
<svg class="h-5 w-5 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<svg class="h-5 w-5 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<svg class="h-5 w-5 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<svg class="h-5 w-5 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -0,0 +1,48 @@
|
||||
<section class="text-gray-600 body-font">
|
||||
<div class="container px-5 py-24 mx-auto">
|
||||
<div class="flex flex-wrap -mx-4 -mb-10 text-center">
|
||||
<div class="sm:w-1/2 mb-10 px-4">
|
||||
<div class="rounded-lg h-64 overflow-hidden">
|
||||
<img
|
||||
alt="content"
|
||||
class="object-cover object-center h-full w-full"
|
||||
src="https://dummyimage.com/1201x501"
|
||||
/>
|
||||
</div>
|
||||
<h2 class="title-font text-2xl font-medium text-gray-900 mt-6 mb-3">
|
||||
Buy YouTube Videos
|
||||
</h2>
|
||||
<p class="leading-relaxed text-base">
|
||||
Williamsburg occupy sustainable snackwave gochujang. Pinterest
|
||||
cornhole brunch, slow-carb neutra irony.
|
||||
</p>
|
||||
<button
|
||||
class="flex mx-auto mt-6 text-white bg-indigo-500 border-0 py-2 px-5 focus:outline-none hover:bg-indigo-600 rounded"
|
||||
>
|
||||
Button
|
||||
</button>
|
||||
</div>
|
||||
<div class="sm:w-1/2 mb-10 px-4">
|
||||
<div class="rounded-lg h-64 overflow-hidden">
|
||||
<img
|
||||
alt="content"
|
||||
class="object-cover object-center h-full w-full"
|
||||
src="https://dummyimage.com/1202x502"
|
||||
/>
|
||||
</div>
|
||||
<h2 class="title-font text-2xl font-medium text-gray-900 mt-6 mb-3">
|
||||
The Catalyzer
|
||||
</h2>
|
||||
<p class="leading-relaxed text-base">
|
||||
Williamsburg occupy sustainable snackwave gochujang. Pinterest
|
||||
cornhole brunch, slow-carb neutra irony.
|
||||
</p>
|
||||
<button
|
||||
class="flex mx-auto mt-6 text-white bg-indigo-500 border-0 py-2 px-5 focus:outline-none hover:bg-indigo-600 rounded"
|
||||
>
|
||||
Button
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -7,9 +7,7 @@ description = "Core macro for Dioxus Virtual DOM"
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
documentation = "https://dioxuslabs.com"
|
||||
keywords = ["dom", "ui", "gui", "react"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
@@ -18,10 +16,11 @@ proc-macro = true
|
||||
proc-macro2 = { version = "1.0" }
|
||||
quote = "1.0"
|
||||
syn = { version = "1.0", features = ["full", "extra-traits"] }
|
||||
dioxus-rsx = { path = "../rsx", version = "^0.0.3" }
|
||||
dioxus-rsx = { workspace = true }
|
||||
|
||||
# testing
|
||||
[dev-dependencies]
|
||||
dioxus = { workspace = true }
|
||||
rustversion = "1.0"
|
||||
trybuild = "1.0"
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::borrow::Borrow;
|
||||
|
||||
use dioxus_core_macro::*;
|
||||
|
||||
#[test]
|
||||
@@ -20,8 +18,8 @@ fn formatting_compiles() {
|
||||
|
||||
// function calls in formatings work
|
||||
assert_eq!(
|
||||
format_args_f!("{x.borrow():?}").to_string(),
|
||||
format!("{:?}", x.borrow())
|
||||
format_args_f!("{blah(&x):?}").to_string(),
|
||||
format!("{:?}", blah(&x))
|
||||
);
|
||||
|
||||
// allows duplicate format args
|
||||
@@ -30,3 +28,7 @@ fn formatting_compiles() {
|
||||
format!("{x:?} {x:?}")
|
||||
);
|
||||
}
|
||||
|
||||
fn blah(hi: &(i32, i32)) -> String {
|
||||
format_args_f!("{hi.0} {hi.1}").to_string()
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#[test]
|
||||
fn rsx() {
|
||||
let t = trybuild::TestCases::new();
|
||||
t.compile_fail("tests/rsx/trailing-comma-0.rs");
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Given an `rsx!` invocation with a missing trailing comma,
|
||||
// ensure the stderr output has an informative span.
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {
|
||||
rsx! {
|
||||
p {
|
||||
class: "foo bar"
|
||||
"Hello world"
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: missing trailing comma
|
||||
--> tests/rsx/trailing-comma-0.rs:9:20
|
||||
|
|
||||
9 | class: "foo bar"
|
||||
| ^^^^^^^^^
|
||||
@@ -7,34 +7,28 @@ description = "Core functionality for Dioxus - a concurrent renderer-agnostic Vi
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
documentation = "https://dioxuslabs.com"
|
||||
keywords = ["dom", "ui", "gui", "react"]
|
||||
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
# Bumpalo is used as a micro heap backing each component
|
||||
bumpalo = { version = "3.6", features = ["collections", "boxed"] }
|
||||
|
||||
# faster hashmaps
|
||||
rustc-hash = "1.1.0"
|
||||
rustc-hash = { workspace = true }
|
||||
|
||||
# Used in diffing
|
||||
longest-increasing-subsequence = "0.1.0"
|
||||
|
||||
futures-util = { version = "0.3", default-features = false, features = [
|
||||
futures-util = { workspace = true, default-features = false, features = [
|
||||
"alloc",
|
||||
] }
|
||||
|
||||
slab = "0.4"
|
||||
slab = { workspace = true }
|
||||
|
||||
futures-channel = "0.3.21"
|
||||
|
||||
indexmap = "1.7"
|
||||
futures-channel = { workspace = true }
|
||||
|
||||
smallbox = "0.8.1"
|
||||
log = "0.4.17"
|
||||
log = { workspace = true }
|
||||
|
||||
# Serialize the Edits for use in Webview/Liveview instances
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
@@ -42,8 +36,8 @@ serde = { version = "1", features = ["derive"], optional = true }
|
||||
bumpslab = { version = "0.2.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
dioxus = { path = "../dioxus" }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
dioxus = { workspace = true }
|
||||
pretty_assertions = "1.3.0"
|
||||
rand = "0.8.5"
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ use crate::{
|
||||
innerlude::DirtyScope, nodes::RenderReturn, nodes::VNode, virtual_dom::VirtualDom,
|
||||
AttributeValue, DynamicNode, ScopeId,
|
||||
};
|
||||
use bumpalo::boxed::Box as BumpBox;
|
||||
|
||||
/// An Element's unique identifier.
|
||||
///
|
||||
@@ -22,7 +21,7 @@ pub(crate) struct ElementRef {
|
||||
pub template: Option<NonNull<VNode<'static>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ElementPath {
|
||||
Deep(&'static [u8]),
|
||||
Root(usize),
|
||||
@@ -110,9 +109,7 @@ impl VirtualDom {
|
||||
|
||||
// Drop all the hooks once the children are dropped
|
||||
// this means we'll drop hooks bottom-up
|
||||
for hook in scope.hook_list.get_mut().drain(..) {
|
||||
drop(unsafe { BumpBox::from_raw(hook) });
|
||||
}
|
||||
scope.hooks.get_mut().clear();
|
||||
|
||||
// Drop all the futures once the hooks are dropped
|
||||
for task_id in scope.spawned_tasks.borrow_mut().drain() {
|
||||
|
||||
@@ -53,7 +53,7 @@ impl<T> Event<T> {
|
||||
/// rsx! {
|
||||
/// button {
|
||||
/// onclick: move |evt: Event<MouseData>| {
|
||||
/// evt.cancel_bubble();
|
||||
/// evt.stop_propagation();
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
|
||||
@@ -38,8 +38,7 @@ impl VirtualDom {
|
||||
node_arena_2: BumpFrame::new(0),
|
||||
spawned_tasks: Default::default(),
|
||||
render_cnt: Default::default(),
|
||||
hook_arena: Default::default(),
|
||||
hook_list: Default::default(),
|
||||
hooks: Default::default(),
|
||||
hook_idx: Default::default(),
|
||||
shared_contexts: Default::default(),
|
||||
borrowed_props: Default::default(),
|
||||
|
||||
+52
-19
@@ -11,11 +11,11 @@ use crate::{
|
||||
};
|
||||
use bumpalo::{boxed::Box as BumpBox, Bump};
|
||||
use bumpslab::{BumpSlab, Slot};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use rustc_hash::FxHashSet;
|
||||
use slab::{Slab, VacantEntry};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::{Cell, RefCell},
|
||||
cell::{Cell, RefCell, UnsafeCell},
|
||||
fmt::{Arguments, Debug},
|
||||
future::Future,
|
||||
ops::{Index, IndexMut},
|
||||
@@ -170,11 +170,10 @@ pub struct ScopeState {
|
||||
|
||||
pub(crate) height: u32,
|
||||
|
||||
pub(crate) hook_arena: Bump,
|
||||
pub(crate) hook_list: RefCell<Vec<*mut dyn Any>>,
|
||||
pub(crate) hooks: RefCell<Vec<Box<UnsafeCell<dyn Any>>>>,
|
||||
pub(crate) hook_idx: Cell<usize>,
|
||||
|
||||
pub(crate) shared_contexts: RefCell<FxHashMap<TypeId, Box<dyn Any>>>,
|
||||
pub(crate) shared_contexts: RefCell<Vec<(TypeId, Box<dyn Any>)>>,
|
||||
|
||||
pub(crate) tasks: Rc<Scheduler>,
|
||||
pub(crate) spawned_tasks: RefCell<FxHashSet<TaskId>>,
|
||||
@@ -348,7 +347,9 @@ impl<'src> ScopeState {
|
||||
pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
|
||||
self.shared_contexts
|
||||
.borrow()
|
||||
.get(&TypeId::of::<T>())?
|
||||
.iter()
|
||||
.find(|(k, _)| *k == TypeId::of::<T>())
|
||||
.map(|(_, v)| v)?
|
||||
.downcast_ref::<T>()
|
||||
.cloned()
|
||||
}
|
||||
@@ -365,8 +366,13 @@ impl<'src> ScopeState {
|
||||
while let Some(parent_ptr) = search_parent {
|
||||
// safety: all parent pointers are valid thanks to the bump arena
|
||||
let parent = unsafe { &*parent_ptr };
|
||||
if let Some(shared) = parent.shared_contexts.borrow().get(&TypeId::of::<T>()) {
|
||||
return shared.downcast_ref::<T>().cloned();
|
||||
if let Some(shared) = parent
|
||||
.shared_contexts
|
||||
.borrow()
|
||||
.iter()
|
||||
.find(|(k, _)| *k == TypeId::of::<T>())
|
||||
{
|
||||
return shared.1.downcast_ref::<T>().cloned();
|
||||
}
|
||||
search_parent = parent.parent;
|
||||
}
|
||||
@@ -395,13 +401,40 @@ impl<'src> ScopeState {
|
||||
/// }
|
||||
/// ```
|
||||
pub fn provide_context<T: 'static + Clone>(&self, value: T) -> T {
|
||||
let value2 = value.clone();
|
||||
let mut contexts = self.shared_contexts.borrow_mut();
|
||||
|
||||
self.shared_contexts
|
||||
.borrow_mut()
|
||||
.insert(TypeId::of::<T>(), Box::new(value));
|
||||
// If the context exists, swap it out for the new value
|
||||
for ctx in contexts.iter_mut() {
|
||||
// Swap the ptr directly
|
||||
if let Some(ctx) = ctx.1.downcast_mut::<T>() {
|
||||
std::mem::swap(ctx, &mut value.clone());
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
value2
|
||||
// Else, just push it
|
||||
contexts.push((TypeId::of::<T>(), Box::new(value.clone())));
|
||||
|
||||
value
|
||||
}
|
||||
|
||||
/// Provide a context to the root and then consume it
|
||||
///
|
||||
/// This is intended for "global" state management solutions that would rather be implicit for the entire app.
|
||||
/// Things like signal runtimes and routers are examples of "singletons" that would benefit from lazy initialization.
|
||||
///
|
||||
/// Note that you should be checking if the context existed before trying to provide a new one. Providing a context
|
||||
/// when a context already exists will swap the context out for the new one, which may not be what you want.
|
||||
pub fn provide_root_context<T: 'static + Clone>(&self, context: T) -> T {
|
||||
let mut parent = self;
|
||||
|
||||
// Walk upwards until there is no more parent - and tada we have the root
|
||||
while let Some(next_parent) = parent.parent {
|
||||
parent = unsafe { &*next_parent };
|
||||
debug_assert_eq!(parent.scope_id(), ScopeId(0));
|
||||
}
|
||||
|
||||
parent.provide_context(context)
|
||||
}
|
||||
|
||||
/// Pushes the future onto the poll queue to be polled after the component renders.
|
||||
@@ -641,18 +674,18 @@ impl<'src> ScopeState {
|
||||
#[allow(clippy::mut_from_ref)]
|
||||
pub fn use_hook<State: 'static>(&self, initializer: impl FnOnce() -> State) -> &mut State {
|
||||
let cur_hook = self.hook_idx.get();
|
||||
let mut hook_list = self.hook_list.try_borrow_mut().expect("The hook list is already borrowed: This error is likely caused by trying to use a hook inside a hook which violates the rules of hooks.");
|
||||
let mut hooks = self.hooks.try_borrow_mut().expect("The hook list is already borrowed: This error is likely caused by trying to use a hook inside a hook which violates the rules of hooks.");
|
||||
|
||||
if cur_hook >= hook_list.len() {
|
||||
hook_list.push(self.hook_arena.alloc(initializer()));
|
||||
if cur_hook >= hooks.len() {
|
||||
hooks.push(Box::new(UnsafeCell::new(initializer())));
|
||||
}
|
||||
|
||||
hook_list
|
||||
hooks
|
||||
.get(cur_hook)
|
||||
.and_then(|inn| {
|
||||
self.hook_idx.set(cur_hook + 1);
|
||||
let raw_box = unsafe { &mut **inn };
|
||||
raw_box.downcast_mut::<State>()
|
||||
let raw_ref = unsafe { &mut *inn.get() };
|
||||
raw_ref.downcast_mut::<State>()
|
||||
})
|
||||
.expect(
|
||||
r###"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use dioxus::prelude::Props;
|
||||
use dioxus_core::*;
|
||||
use std::cell::Cell;
|
||||
use std::{cell::Cell, collections::HashSet};
|
||||
|
||||
fn random_ns() -> Option<&'static str> {
|
||||
let namespace = rand::random::<u8>() % 2;
|
||||
@@ -165,7 +165,7 @@ fn create_random_template(name: &'static str) -> (Template<'static>, Vec<Dynamic
|
||||
}
|
||||
|
||||
fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode {
|
||||
let range = if depth > 3 { 1 } else { 3 };
|
||||
let range = if depth > 5 { 1 } else { 4 };
|
||||
match rand::random::<u8>() % range {
|
||||
0 => DynamicNode::Placeholder(Default::default()),
|
||||
1 => cx.make_node((0..(rand::random::<u8>() % 5)).map(|_| VNode {
|
||||
@@ -190,12 +190,21 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode {
|
||||
DepthProps { depth, root: false },
|
||||
"create_random_element",
|
||||
),
|
||||
3 => {
|
||||
let data = String::from("borrowed data");
|
||||
let bumpped = cx.bump().alloc(data);
|
||||
cx.component(
|
||||
create_random_element_borrowed,
|
||||
BorrowedDepthProps { borrow: &*bumpped, inner: DepthProps { depth, root: false } },
|
||||
"create_random_element_borrowed",
|
||||
)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
|
||||
let value = match rand::random::<u8>() % 6 {
|
||||
let value = match rand::random::<u8>() % 7 {
|
||||
0 => AttributeValue::Text(Box::leak(
|
||||
format!("{}", rand::random::<usize>()).into_boxed_str(),
|
||||
)),
|
||||
@@ -204,7 +213,16 @@ fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
|
||||
3 => AttributeValue::Bool(rand::random()),
|
||||
4 => cx.any_value(rand::random::<usize>()),
|
||||
5 => AttributeValue::None,
|
||||
// Listener(RefCell<Option<ListenerCb<'a>>>),
|
||||
6 => {
|
||||
let value = cx.listener(|e: Event<String>| println!("{:?}", e));
|
||||
return Attribute {
|
||||
name: "ondata",
|
||||
value,
|
||||
namespace: None,
|
||||
mounted_element: Default::default(),
|
||||
volatile: false,
|
||||
};
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
Attribute {
|
||||
@@ -218,6 +236,19 @@ fn create_random_dynamic_attr(cx: &ScopeState) -> Attribute {
|
||||
|
||||
static mut TEMPLATE_COUNT: usize = 0;
|
||||
|
||||
#[derive(Props)]
|
||||
struct BorrowedDepthProps<'a> {
|
||||
borrow: &'a str,
|
||||
inner: DepthProps,
|
||||
}
|
||||
|
||||
fn create_random_element_borrowed<'a>(cx: Scope<'a, BorrowedDepthProps<'a>>) -> Element<'a> {
|
||||
println!("{}", cx.props.borrow);
|
||||
let bump = cx.bump();
|
||||
let allocated = bump.alloc(Scoped { scope: cx, props: &cx.props.inner });
|
||||
create_random_element(allocated)
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Props)]
|
||||
struct DepthProps {
|
||||
depth: usize,
|
||||
@@ -225,7 +256,9 @@ struct DepthProps {
|
||||
}
|
||||
|
||||
fn create_random_element(cx: Scope<DepthProps>) -> Element {
|
||||
cx.needs_update();
|
||||
if rand::random::<usize>() % 10 == 0 {
|
||||
cx.needs_update();
|
||||
}
|
||||
let range = if cx.props.root { 2 } else { 3 };
|
||||
let node = match rand::random::<usize>() % range {
|
||||
0 | 1 => {
|
||||
@@ -243,7 +276,7 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
|
||||
)
|
||||
.into_boxed_str(),
|
||||
));
|
||||
println!("{template:#?}");
|
||||
// println!("{template:#?}");
|
||||
let node = VNode {
|
||||
key: None,
|
||||
parent: None,
|
||||
@@ -276,14 +309,14 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
println!("{node:#?}");
|
||||
// println!("{node:#?}");
|
||||
node
|
||||
}
|
||||
|
||||
// test for panics when creating random nodes and templates
|
||||
#[test]
|
||||
fn create() {
|
||||
for _ in 0..100 {
|
||||
for _ in 0..1000 {
|
||||
let mut vdom =
|
||||
VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true });
|
||||
let _ = vdom.rebuild();
|
||||
@@ -294,12 +327,32 @@ fn create() {
|
||||
// This test will change the template every render which is not very realistic, but it helps stress the system
|
||||
#[test]
|
||||
fn diff() {
|
||||
for _ in 0..10 {
|
||||
for _ in 0..100000 {
|
||||
let mut vdom =
|
||||
VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true });
|
||||
let _ = vdom.rebuild();
|
||||
for _ in 0..10 {
|
||||
let _ = vdom.render_immediate();
|
||||
// A list of all elements that have had event listeners
|
||||
// This is intentionally never cleared, so that we can test that calling event listeners that are removed doesn't cause a panic
|
||||
let mut event_listeners = HashSet::new();
|
||||
for _ in 0..100 {
|
||||
for &id in &event_listeners {
|
||||
println!("firing event on {:?}", id);
|
||||
vdom.handle_event(
|
||||
"data",
|
||||
std::rc::Rc::new(String::from("hello world")),
|
||||
id,
|
||||
true,
|
||||
);
|
||||
}
|
||||
{
|
||||
let muts = vdom.render_immediate();
|
||||
for mut_ in muts.edits {
|
||||
if let Mutation::NewEventListener { name, id } = mut_ {
|
||||
println!("new event listener on {:?} for {:?}", id, name);
|
||||
event_listeners.insert(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
-19
@@ -3,49 +3,51 @@ name = "dioxus-desktop"
|
||||
version = "0.3.0"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2018"
|
||||
description = "Dioxus VirtualDOM renderer for a remote webview instance"
|
||||
description = "WebView renderer for Dioxus"
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
documentation = "https://dioxuslabs.com"
|
||||
homepage = "https://dioxuslabs.com/docs/0.3/guide/en/getting_started/desktop.html"
|
||||
keywords = ["dom", "ui", "gui", "react"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
|
||||
dioxus-html = { path = "../html", features = ["serialize", "native-bind"], version = "^0.3.0" }
|
||||
dioxus-interpreter-js = { path = "../interpreter", version = "^0.3.0" }
|
||||
dioxus-hot-reload = { path = "../hot-reload", optional = true }
|
||||
dioxus-core = { workspace = true, features = ["serialize"] }
|
||||
dioxus-html = { workspace = true, features = ["serialize", "native-bind"] }
|
||||
dioxus-interpreter-js = { workspace = true }
|
||||
dioxus-hot-reload = { workspace = true, optional = true }
|
||||
|
||||
serde = "1.0.136"
|
||||
serde_json = "1.0.79"
|
||||
thiserror = "1.0.30"
|
||||
log = "0.4.14"
|
||||
wry = { version = "0.27.2" }
|
||||
futures-channel = "0.3.21"
|
||||
tokio = { version = "1.27", features = [
|
||||
log = { workspace = true }
|
||||
wry = { version = "0.28.0" }
|
||||
futures-channel = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
"sync",
|
||||
"rt-multi-thread",
|
||||
"rt",
|
||||
"time",
|
||||
"macros",
|
||||
"fs",
|
||||
], optional = true, default-features = false }
|
||||
], optional = true }
|
||||
webbrowser = "0.8.0"
|
||||
infer = "0.11.0"
|
||||
dunce = "1.0.2"
|
||||
slab = "0.4"
|
||||
slab = { workspace = true }
|
||||
|
||||
futures-util = "0.3.25"
|
||||
rfd = "0.11.3"
|
||||
futures-util = { workspace = true }
|
||||
urlencoding = "2.1.2"
|
||||
|
||||
|
||||
[target.'cfg(any(target_os = "windows",target_os = "macos",target_os = "linux",target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies]
|
||||
rfd = "0.11.3"
|
||||
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
objc = "0.2.7"
|
||||
objc_id = "0.1.1"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = "0.9.3"
|
||||
objc = "0.2.7"
|
||||
|
||||
[features]
|
||||
default = ["tokio_runtime", "hot-reload"]
|
||||
@@ -56,9 +58,9 @@ tray = ["wry/tray"]
|
||||
hot-reload = ["dioxus-hot-reload"]
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus-core-macro = { path = "../core-macro" }
|
||||
dioxus-hooks = { path = "../hooks" }
|
||||
dioxus = { path = "../dioxus" }
|
||||
dioxus-core-macro = { workspace = true }
|
||||
dioxus-hooks = { workspace = true }
|
||||
dioxus = { workspace = true }
|
||||
exitcode = "1.1.2"
|
||||
scraper = "0.16.0"
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ pub(crate) fn check_app_exits(app: Component) {
|
||||
Config::new().with_window(WindowBuilder::new().with_visible(false)),
|
||||
);
|
||||
|
||||
// Stop deadman's switch
|
||||
should_panic.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,17 @@ use wry::{
|
||||
|
||||
// pub(crate) type DynEventHandlerFn = dyn Fn(&mut EventLoop<()>, &mut WebView);
|
||||
|
||||
/// The behaviour of the application when the last window is closed.
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub enum WindowCloseBehaviour {
|
||||
/// Default behaviour, closing the last window exits the app
|
||||
LastWindowExitsApp,
|
||||
/// Closing the last window will not actually close it, just hide it
|
||||
LastWindowHides,
|
||||
/// Closing the last window will close it but the app will keep running so that new windows can be opened
|
||||
CloseWindow,
|
||||
}
|
||||
|
||||
/// The configuration for the desktop application.
|
||||
pub struct Config {
|
||||
pub(crate) window: WindowBuilder,
|
||||
@@ -23,6 +34,8 @@ pub struct Config {
|
||||
pub(crate) custom_head: Option<String>,
|
||||
pub(crate) custom_index: Option<String>,
|
||||
pub(crate) root_name: String,
|
||||
pub(crate) background_color: Option<(u8, u8, u8, u8)>,
|
||||
pub(crate) last_window_close_behaviour: WindowCloseBehaviour,
|
||||
}
|
||||
|
||||
type DropHandler = Box<dyn Fn(&Window, FileDropEvent) -> bool>;
|
||||
@@ -50,6 +63,8 @@ impl Config {
|
||||
custom_head: None,
|
||||
custom_index: None,
|
||||
root_name: "main".to_string(),
|
||||
background_color: None,
|
||||
last_window_close_behaviour: WindowCloseBehaviour::LastWindowExitsApp,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +102,12 @@ impl Config {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the behaviour of the application when the last window is closed.
|
||||
pub fn with_close_behaviour(mut self, behaviour: WindowCloseBehaviour) -> Self {
|
||||
self.last_window_close_behaviour = behaviour;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a file drop handler
|
||||
pub fn with_file_drop_handler(
|
||||
mut self,
|
||||
@@ -137,6 +158,14 @@ impl Config {
|
||||
self.root_name = name.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the background color of the WebView.
|
||||
/// This will be set before the HTML is rendered and can be used to prevent flashing when the page loads.
|
||||
/// Accepts a color in RGBA format
|
||||
pub fn with_background_color(mut self, color: (u8, u8, u8, u8)) -> Self {
|
||||
self.background_color = Some(color);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
|
||||
@@ -6,8 +6,6 @@ use crate::create_new_window;
|
||||
use crate::eval::EvalResult;
|
||||
use crate::events::IpcMessage;
|
||||
use crate::query::QueryEngine;
|
||||
use crate::shortcut::IntoKeyCode;
|
||||
use crate::shortcut::IntoModifersState;
|
||||
use crate::shortcut::ShortcutId;
|
||||
use crate::shortcut::ShortcutRegistry;
|
||||
use crate::shortcut::ShortcutRegistryError;
|
||||
@@ -18,6 +16,7 @@ use dioxus_core::VirtualDom;
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
use dioxus_hot_reload::HotReloadMsg;
|
||||
use slab::Slab;
|
||||
use wry::application::accelerator::Accelerator;
|
||||
use wry::application::event::Event;
|
||||
use wry::application::event_loop::EventLoopProxy;
|
||||
use wry::application::event_loop::EventLoopWindowTarget;
|
||||
@@ -51,8 +50,7 @@ pub(crate) type WebviewQueue = Rc<RefCell<Vec<WebviewHandler>>>;
|
||||
/// ```rust, ignore
|
||||
/// let desktop = cx.consume_context::<DesktopContext>().unwrap();
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct DesktopContext {
|
||||
pub struct DesktopService {
|
||||
/// The wry/tao proxy to the current window
|
||||
pub webview: Rc<WebView>,
|
||||
|
||||
@@ -74,8 +72,11 @@ pub struct DesktopContext {
|
||||
pub(crate) views: Rc<RefCell<Vec<*mut objc::runtime::Object>>>,
|
||||
}
|
||||
|
||||
/// A handle to the [`DesktopService`] that can be passed around.
|
||||
pub type DesktopContext = Rc<DesktopService>;
|
||||
|
||||
/// A smart pointer to the current window.
|
||||
impl std::ops::Deref for DesktopContext {
|
||||
impl std::ops::Deref for DesktopService {
|
||||
type Target = Window;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
@@ -83,9 +84,9 @@ impl std::ops::Deref for DesktopContext {
|
||||
}
|
||||
}
|
||||
|
||||
impl DesktopContext {
|
||||
impl DesktopService {
|
||||
pub(crate) fn new(
|
||||
webview: Rc<WebView>,
|
||||
webview: WebView,
|
||||
proxy: ProxyType,
|
||||
event_loop: EventLoopWindowTarget<UserWindowEvent>,
|
||||
webviews: WebviewQueue,
|
||||
@@ -93,7 +94,7 @@ impl DesktopContext {
|
||||
shortcut_manager: ShortcutRegistry,
|
||||
) -> Self {
|
||||
Self {
|
||||
webview,
|
||||
webview: Rc::new(webview),
|
||||
proxy,
|
||||
event_loop,
|
||||
query: Default::default(),
|
||||
@@ -112,7 +113,7 @@ impl DesktopContext {
|
||||
/// You can use this to control other windows from the current window.
|
||||
///
|
||||
/// Be careful to not create a cycle of windows, or you might leak memory.
|
||||
pub fn new_window(&self, dom: VirtualDom, cfg: Config) -> Weak<WebView> {
|
||||
pub fn new_window(&self, dom: VirtualDom, cfg: Config) -> Weak<DesktopService> {
|
||||
let window = create_new_window(
|
||||
cfg,
|
||||
&self.event_loop,
|
||||
@@ -123,7 +124,13 @@ impl DesktopContext {
|
||||
self.shortcut_manager.clone(),
|
||||
);
|
||||
|
||||
let id = window.webview.window().id();
|
||||
let desktop_context = window
|
||||
.dom
|
||||
.base_scope()
|
||||
.consume_context::<Rc<DesktopService>>()
|
||||
.unwrap();
|
||||
|
||||
let id = window.desktop_context.webview.window().id();
|
||||
|
||||
self.proxy
|
||||
.send_event(UserWindowEvent(EventData::NewWindow, id))
|
||||
@@ -133,11 +140,9 @@ impl DesktopContext {
|
||||
.send_event(UserWindowEvent(EventData::Poll, id))
|
||||
.unwrap();
|
||||
|
||||
let webview = window.webview.clone();
|
||||
|
||||
self.pending_windows.borrow_mut().push(window);
|
||||
|
||||
Rc::downgrade(&webview)
|
||||
Rc::downgrade(&desktop_context)
|
||||
}
|
||||
|
||||
/// trigger the drag-window event
|
||||
@@ -237,15 +242,11 @@ impl DesktopContext {
|
||||
/// Linux: Only works on x11. See [this issue](https://github.com/tauri-apps/tao/issues/331) for more information.
|
||||
pub fn create_shortcut(
|
||||
&self,
|
||||
key: impl IntoKeyCode,
|
||||
modifiers: impl IntoModifersState,
|
||||
accelerator: Accelerator,
|
||||
callback: impl FnMut() + 'static,
|
||||
) -> Result<ShortcutId, ShortcutRegistryError> {
|
||||
self.shortcut_manager.add_shortcut(
|
||||
modifiers.into_modifiers_state(),
|
||||
key.into_key_code(),
|
||||
Box::new(callback),
|
||||
)
|
||||
self.shortcut_manager
|
||||
.add_shortcut(accelerator, Box::new(callback))
|
||||
}
|
||||
|
||||
/// Remove a global shortcut
|
||||
@@ -405,7 +406,7 @@ pub fn use_wry_event_handler(
|
||||
let id = desktop.create_wry_event_handler(handler);
|
||||
|
||||
WryEventHandler {
|
||||
handlers: desktop.event_handlers,
|
||||
handlers: desktop.event_handlers.clone(),
|
||||
id,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use dioxus_core::ElementId;
|
||||
use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking};
|
||||
use wry::webview::WebView;
|
||||
|
||||
use crate::query::QueryEngine;
|
||||
use crate::{desktop_context::DesktopContext, query::QueryEngine};
|
||||
|
||||
/// A mounted element passed to onmounted events
|
||||
pub struct DesktopElement {
|
||||
id: ElementId,
|
||||
webview: Rc<WebView>,
|
||||
webview: DesktopContext,
|
||||
query: QueryEngine,
|
||||
}
|
||||
|
||||
impl DesktopElement {
|
||||
pub(crate) fn new(id: ElementId, webview: Rc<WebView>, query: QueryEngine) -> Self {
|
||||
pub(crate) fn new(id: ElementId, webview: DesktopContext, query: QueryEngine) -> Self {
|
||||
Self { id, webview, query }
|
||||
}
|
||||
}
|
||||
@@ -37,7 +34,7 @@ impl RenderedElementBacking for DesktopElement {
|
||||
|
||||
let fut = self
|
||||
.query
|
||||
.new_query::<Option<Rect<f64, f64>>>(&script, &self.webview)
|
||||
.new_query::<Option<Rect<f64, f64>>>(&script, &self.webview.webview)
|
||||
.resolve();
|
||||
Box::pin(async move {
|
||||
match fut.await {
|
||||
@@ -64,7 +61,7 @@ impl RenderedElementBacking for DesktopElement {
|
||||
|
||||
let fut = self
|
||||
.query
|
||||
.new_query::<bool>(&script, &self.webview)
|
||||
.new_query::<bool>(&script, &self.webview.webview)
|
||||
.resolve();
|
||||
Box::pin(async move {
|
||||
match fut.await {
|
||||
@@ -90,7 +87,7 @@ impl RenderedElementBacking for DesktopElement {
|
||||
|
||||
let fut = self
|
||||
.query
|
||||
.new_query::<bool>(&script, &self.webview)
|
||||
.new_query::<bool>(&script, &self.webview.webview)
|
||||
.resolve();
|
||||
|
||||
Box::pin(async move {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::{path::PathBuf, str::FromStr};
|
||||
#![allow(unused)]
|
||||
|
||||
use serde::Deserialize;
|
||||
use std::{path::PathBuf, str::FromStr};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct FileDiologRequest {
|
||||
pub(crate) struct FileDialogRequest {
|
||||
#[serde(default)]
|
||||
accept: Option<String>,
|
||||
multiple: bool,
|
||||
@@ -12,7 +13,29 @@ pub(crate) struct FileDiologRequest {
|
||||
pub bubbles: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn get_file_event(request: &FileDiologRequest) -> Vec<PathBuf> {
|
||||
#[cfg(not(any(
|
||||
target_os = "windows",
|
||||
target_os = "macos",
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
)))]
|
||||
pub(crate) fn get_file_event(_request: &FileDialogRequest) -> Vec<PathBuf> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
target_os = "macos",
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
pub(crate) fn get_file_event(request: &FileDialogRequest) -> Vec<PathBuf> {
|
||||
let mut dialog = rfd::FileDialog::new();
|
||||
|
||||
let filters: Vec<_> = request
|
||||
|
||||
+115
-46
@@ -16,10 +16,14 @@ mod shortcut;
|
||||
mod waker;
|
||||
mod webview;
|
||||
|
||||
#[cfg(any(target_os = "ios", target_os = "android"))]
|
||||
mod mobile_shortcut;
|
||||
|
||||
use crate::query::QueryResult;
|
||||
pub use cfg::Config;
|
||||
pub use cfg::{Config, WindowCloseBehaviour};
|
||||
pub use desktop_context::DesktopContext;
|
||||
pub use desktop_context::{
|
||||
use_window, use_wry_event_handler, DesktopContext, WryEventHandler, WryEventHandlerId,
|
||||
use_window, use_wry_event_handler, DesktopService, WryEventHandler, WryEventHandlerId,
|
||||
};
|
||||
use desktop_context::{EventData, UserWindowEvent, WebviewQueue, WindowEventHandlers};
|
||||
use dioxus_core::*;
|
||||
@@ -30,6 +34,7 @@ pub use eval::{use_eval, EvalResult};
|
||||
use futures_util::{pin_mut, FutureExt};
|
||||
use shortcut::ShortcutRegistry;
|
||||
pub use shortcut::{use_global_shortcut, ShortcutHandle, ShortcutId, ShortcutRegistryError};
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
use std::task::Waker;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
@@ -117,17 +122,19 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
||||
|
||||
let proxy = event_loop.create_proxy();
|
||||
|
||||
let window_behaviour = cfg.last_window_close_behaviour;
|
||||
|
||||
// Intialize hot reloading if it is enabled
|
||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||
{
|
||||
dioxus_hot_reload::connect({
|
||||
let proxy = proxy.clone();
|
||||
dioxus_hot_reload::connect(move |template| {
|
||||
move |template| {
|
||||
let _ = proxy.send_event(UserWindowEvent(
|
||||
EventData::HotReloadEvent(template),
|
||||
unsafe { WindowId::dummy() },
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// We start the tokio runtime *on this thread*
|
||||
// Any future we poll later will use this runtime to spawn tasks and for IO
|
||||
@@ -150,16 +157,10 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
||||
|
||||
let shortcut_manager = ShortcutRegistry::new(&event_loop);
|
||||
|
||||
// By default, we'll create a new window when the app starts
|
||||
queue.borrow_mut().push(create_new_window(
|
||||
cfg,
|
||||
&event_loop,
|
||||
&proxy,
|
||||
VirtualDom::new_with_props(root, props),
|
||||
&queue,
|
||||
&event_handlers,
|
||||
shortcut_manager.clone(),
|
||||
));
|
||||
// move the props into a cell so we can pop it out later to create the first window
|
||||
// iOS panics if we create a window before the event loop is started
|
||||
let props = Rc::new(Cell::new(Some(props)));
|
||||
let cfg = Rc::new(Cell::new(Some(cfg)));
|
||||
|
||||
event_loop.run(move |window_event, event_loop, control_flow| {
|
||||
*control_flow = ControlFlow::Wait;
|
||||
@@ -170,27 +171,61 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
||||
Event::WindowEvent {
|
||||
event, window_id, ..
|
||||
} => match event {
|
||||
WindowEvent::CloseRequested => {
|
||||
webviews.remove(&window_id);
|
||||
WindowEvent::CloseRequested => match window_behaviour {
|
||||
cfg::WindowCloseBehaviour::LastWindowExitsApp => {
|
||||
webviews.remove(&window_id);
|
||||
|
||||
if webviews.is_empty() {
|
||||
*control_flow = ControlFlow::Exit
|
||||
if webviews.is_empty() {
|
||||
*control_flow = ControlFlow::Exit
|
||||
}
|
||||
}
|
||||
}
|
||||
cfg::WindowCloseBehaviour::LastWindowHides => {
|
||||
let Some(webview) = webviews.get(&window_id) else {
|
||||
return;
|
||||
};
|
||||
hide_app_window(&webview.desktop_context.webview);
|
||||
}
|
||||
cfg::WindowCloseBehaviour::CloseWindow => {
|
||||
webviews.remove(&window_id);
|
||||
}
|
||||
},
|
||||
WindowEvent::Destroyed { .. } => {
|
||||
webviews.remove(&window_id);
|
||||
|
||||
if webviews.is_empty() {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
if matches!(
|
||||
window_behaviour,
|
||||
cfg::WindowCloseBehaviour::LastWindowExitsApp
|
||||
) && webviews.is_empty()
|
||||
{
|
||||
*control_flow = ControlFlow::Exit
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
|
||||
Event::NewEvents(StartCause::Init)
|
||||
| Event::UserEvent(UserWindowEvent(EventData::NewWindow, _)) => {
|
||||
Event::NewEvents(StartCause::Init) => {
|
||||
//
|
||||
let props = props.take().unwrap();
|
||||
let cfg = cfg.take().unwrap();
|
||||
|
||||
let handler = create_new_window(
|
||||
cfg,
|
||||
event_loop,
|
||||
&proxy,
|
||||
VirtualDom::new_with_props(root, props),
|
||||
&queue,
|
||||
&event_handlers,
|
||||
shortcut_manager.clone(),
|
||||
);
|
||||
|
||||
let id = handler.desktop_context.webview.window().id();
|
||||
webviews.insert(id, handler);
|
||||
_ = proxy.send_event(UserWindowEvent(EventData::Poll, id));
|
||||
}
|
||||
|
||||
Event::UserEvent(UserWindowEvent(EventData::NewWindow, _)) => {
|
||||
for handler in queue.borrow_mut().drain(..) {
|
||||
let id = handler.webview.window().id();
|
||||
let id = handler.desktop_context.webview.window().id();
|
||||
webviews.insert(id, handler);
|
||||
_ = proxy.send_event(UserWindowEvent(EventData::Poll, id));
|
||||
}
|
||||
@@ -249,9 +284,11 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
||||
.base_scope()
|
||||
.consume_context::<DesktopContext>()
|
||||
.unwrap()
|
||||
.query;
|
||||
.query
|
||||
.clone();
|
||||
|
||||
let element = DesktopElement::new(element, view.webview.clone(), query);
|
||||
let element =
|
||||
DesktopElement::new(element, view.desktop_context.clone(), query);
|
||||
|
||||
Rc::new(MountedData::new(element))
|
||||
} else {
|
||||
@@ -260,7 +297,7 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
||||
|
||||
view.dom.handle_event(&name, as_any, element, bubbles);
|
||||
|
||||
send_edits(view.dom.render_immediate(), &view.webview);
|
||||
send_edits(view.dom.render_immediate(), &view.desktop_context.webview);
|
||||
}
|
||||
|
||||
// When the webview sends a query, we need to send it to the query manager which handles dispatching the data to the correct pending query
|
||||
@@ -274,7 +311,8 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
||||
.base_scope()
|
||||
.consume_context::<DesktopContext>()
|
||||
.unwrap()
|
||||
.query;
|
||||
.query
|
||||
.clone();
|
||||
|
||||
query.send(result);
|
||||
}
|
||||
@@ -282,7 +320,7 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
||||
|
||||
EventData::Ipc(msg) if msg.method() == "initialize" => {
|
||||
let view = webviews.get_mut(&event.1).unwrap();
|
||||
send_edits(view.dom.rebuild(), &view.webview);
|
||||
send_edits(view.dom.rebuild(), &view.desktop_context.webview);
|
||||
}
|
||||
|
||||
EventData::Ipc(msg) if msg.method() == "browser_open" => {
|
||||
@@ -298,7 +336,7 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
||||
|
||||
EventData::Ipc(msg) if msg.method() == "file_diolog" => {
|
||||
if let Ok(file_diolog) =
|
||||
serde_json::from_value::<file_upload::FileDiologRequest>(msg.params())
|
||||
serde_json::from_value::<file_upload::FileDialogRequest>(msg.params())
|
||||
{
|
||||
let id = ElementId(file_diolog.target);
|
||||
let event_name = &file_diolog.event;
|
||||
@@ -320,7 +358,7 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
|
||||
view.dom.handle_event(event_name, data, id, event_bubbles);
|
||||
}
|
||||
|
||||
send_edits(view.dom.render_immediate(), &view.webview);
|
||||
send_edits(view.dom.render_immediate(), &view.desktop_context.webview);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,9 +380,8 @@ fn create_new_window(
|
||||
shortcut_manager: ShortcutRegistry,
|
||||
) -> WebviewHandler {
|
||||
let (webview, web_context) = webview::build(&mut cfg, event_loop, proxy.clone());
|
||||
|
||||
dom.base_scope().provide_context(DesktopContext::new(
|
||||
webview.clone(),
|
||||
let desktop_context = Rc::from(DesktopService::new(
|
||||
webview,
|
||||
proxy.clone(),
|
||||
event_loop.clone(),
|
||||
queue.clone(),
|
||||
@@ -352,24 +389,25 @@ fn create_new_window(
|
||||
shortcut_manager,
|
||||
));
|
||||
|
||||
let id = webview.window().id();
|
||||
dom.base_scope().provide_context(desktop_context.clone());
|
||||
|
||||
// We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both
|
||||
WebviewHandler {
|
||||
webview,
|
||||
// We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both
|
||||
waker: waker::tao_waker(proxy, desktop_context.webview.window().id()),
|
||||
desktop_context,
|
||||
dom,
|
||||
waker: waker::tao_waker(proxy, id),
|
||||
web_context,
|
||||
_web_context: web_context,
|
||||
}
|
||||
}
|
||||
|
||||
struct WebviewHandler {
|
||||
dom: VirtualDom,
|
||||
webview: Rc<wry::webview::WebView>,
|
||||
desktop_context: DesktopContext,
|
||||
waker: Waker,
|
||||
// This is nessisary because of a bug in wry. Wry assumes the webcontext is alive for the lifetime of the webview. We need to keep the webcontext alive, otherwise the webview will crash
|
||||
#[allow(dead_code)]
|
||||
web_context: WebContext,
|
||||
|
||||
// Wry assumes the webcontext is alive for the lifetime of the webview.
|
||||
// We need to keep the webcontext alive, otherwise the webview will crash
|
||||
_web_context: WebContext,
|
||||
}
|
||||
|
||||
/// Poll the virtualdom until it's pending
|
||||
@@ -391,7 +429,7 @@ fn poll_vdom(view: &mut WebviewHandler) {
|
||||
}
|
||||
}
|
||||
|
||||
send_edits(view.dom.render_immediate(), &view.webview);
|
||||
send_edits(view.dom.render_immediate(), &view.desktop_context.webview);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,3 +440,34 @@ fn send_edits(edits: Mutations, webview: &WebView) {
|
||||
// todo: use SSE and binary data to send the edits with lower overhead
|
||||
_ = webview.evaluate_script(&format!("window.interpreter.handleEdits({serialized})"));
|
||||
}
|
||||
|
||||
/// Different hide implementations per platform
|
||||
#[allow(unused)]
|
||||
fn hide_app_window(webview: &WebView) {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use wry::application::platform::windows::WindowExtWindows;
|
||||
webview.window().set_visible(false);
|
||||
webview.window().set_skip_taskbar(true);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use wry::application::platform::unix::WindowExtUnix;
|
||||
webview.window().set_visible(false);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// webview.window().set_visible(false); has the wrong behaviour on macOS
|
||||
// It will hide the window but not show it again when the user switches
|
||||
// back to the app. `NSApplication::hide:` has the correct behaviour
|
||||
use objc::runtime::Object;
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
objc::rc::autoreleasepool(|| unsafe {
|
||||
let app: *mut Object = msg_send![objc::class!(NSApplication), sharedApplication];
|
||||
let nil = std::ptr::null_mut::<Object>();
|
||||
let _: () = msg_send![app, hide: nil];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
#![allow(unused)]
|
||||
|
||||
use super::*;
|
||||
use wry::application::accelerator::Accelerator;
|
||||
use wry::application::event_loop::EventLoopWindowTarget;
|
||||
|
||||
pub struct GlobalShortcut();
|
||||
pub struct ShortcutManager();
|
||||
|
||||
impl ShortcutManager {
|
||||
pub fn new<T>(target: &EventLoopWindowTarget<T>) -> Self {
|
||||
Self()
|
||||
}
|
||||
|
||||
pub fn register(
|
||||
&mut self,
|
||||
accelerator: Accelerator,
|
||||
) -> Result<GlobalShortcut, ShortcutManagerError> {
|
||||
Ok(GlobalShortcut())
|
||||
}
|
||||
|
||||
pub fn unregister(&mut self, id: ShortcutId) -> Result<(), ShortcutManagerError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn unregister_all(&mut self) -> Result<(), ShortcutManagerError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
use std::{error, fmt};
|
||||
|
||||
/// An error whose cause the `ShortcutManager` to fail.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug)]
|
||||
pub enum ShortcutManagerError {
|
||||
AcceleratorAlreadyRegistered(Accelerator),
|
||||
AcceleratorNotRegistered(Accelerator),
|
||||
InvalidAccelerator(String),
|
||||
}
|
||||
|
||||
impl error::Error for ShortcutManagerError {}
|
||||
impl fmt::Display for ShortcutManagerError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
match self {
|
||||
ShortcutManagerError::AcceleratorAlreadyRegistered(e) => {
|
||||
f.pad(&format!("hotkey already registered: {:?}", e))
|
||||
}
|
||||
ShortcutManagerError::AcceleratorNotRegistered(e) => {
|
||||
f.pad(&format!("hotkey not registered: {:?}", e))
|
||||
}
|
||||
ShortcutManagerError::InvalidAccelerator(e) => e.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc};
|
||||
use std::{cell::RefCell, collections::HashMap, rc::Rc, str::FromStr};
|
||||
|
||||
use dioxus_core::ScopeState;
|
||||
use dioxus_html::input_data::keyboard_types::Modifiers;
|
||||
@@ -6,11 +6,24 @@ use slab::Slab;
|
||||
use wry::application::{
|
||||
accelerator::{Accelerator, AcceleratorId},
|
||||
event_loop::EventLoopWindowTarget,
|
||||
global_shortcut::{GlobalShortcut, ShortcutManager, ShortcutManagerError},
|
||||
keyboard::{KeyCode, ModifiersState},
|
||||
};
|
||||
|
||||
use crate::{use_window, DesktopContext};
|
||||
use crate::{desktop_context::DesktopContext, use_window};
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
target_os = "macos",
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
use wry::application::global_shortcut::{GlobalShortcut, ShortcutManager, ShortcutManagerError};
|
||||
|
||||
#[cfg(any(target_os = "ios", target_os = "android"))]
|
||||
pub use crate::mobile_shortcut::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ShortcutRegistry {
|
||||
@@ -21,6 +34,7 @@ pub(crate) struct ShortcutRegistry {
|
||||
type ShortcutMap = Rc<RefCell<HashMap<AcceleratorId, Shortcut>>>;
|
||||
|
||||
struct Shortcut {
|
||||
#[allow(unused)]
|
||||
shortcut: GlobalShortcut,
|
||||
callbacks: Slab<Box<dyn FnMut()>>,
|
||||
}
|
||||
@@ -57,11 +71,9 @@ impl ShortcutRegistry {
|
||||
|
||||
pub(crate) fn add_shortcut(
|
||||
&self,
|
||||
modifiers: impl Into<Option<ModifiersState>>,
|
||||
key: KeyCode,
|
||||
accelerator: Accelerator,
|
||||
callback: Box<dyn FnMut()>,
|
||||
) -> Result<ShortcutId, ShortcutRegistryError> {
|
||||
let accelerator = Accelerator::new(modifiers, key);
|
||||
let accelerator_id = accelerator.clone().id();
|
||||
let mut shortcuts = self.shortcuts.borrow_mut();
|
||||
Ok(
|
||||
@@ -100,8 +112,17 @@ impl ShortcutRegistry {
|
||||
if let Some(callbacks) = shortcuts.get_mut(&id.id) {
|
||||
callbacks.remove(id.number);
|
||||
if callbacks.is_empty() {
|
||||
if let Some(shortcut) = shortcuts.remove(&id.id) {
|
||||
let _ = self.manager.borrow_mut().unregister(shortcut.shortcut);
|
||||
if let Some(_shortcut) = shortcuts.remove(&id.id) {
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
target_os = "macos",
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
let _ = self.manager.borrow_mut().unregister(_shortcut.shortcut);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,12 +132,6 @@ impl ShortcutRegistry {
|
||||
let mut shortcuts = self.shortcuts.borrow_mut();
|
||||
shortcuts.clear();
|
||||
let _ = self.manager.borrow_mut().unregister_all();
|
||||
// prevent CTRL+R from reloading the page which breaks apps
|
||||
let _ = self.add_shortcut(
|
||||
Some(ModifiersState::CONTROL),
|
||||
KeyCode::KeyR,
|
||||
Box::new(|| {}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,22 +159,45 @@ pub struct ShortcutHandle {
|
||||
pub shortcut_id: ShortcutId,
|
||||
}
|
||||
|
||||
pub trait IntoAccelerator {
|
||||
fn accelerator(&self) -> Accelerator;
|
||||
}
|
||||
|
||||
impl IntoAccelerator for (dioxus_html::KeyCode, ModifiersState) {
|
||||
fn accelerator(&self) -> Accelerator {
|
||||
Accelerator::new(Some(self.1), self.0.into_key_code())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAccelerator for (ModifiersState, dioxus_html::KeyCode) {
|
||||
fn accelerator(&self) -> Accelerator {
|
||||
Accelerator::new(Some(self.0), self.1.into_key_code())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAccelerator for dioxus_html::KeyCode {
|
||||
fn accelerator(&self) -> Accelerator {
|
||||
Accelerator::new(None, self.into_key_code())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAccelerator for &str {
|
||||
fn accelerator(&self) -> Accelerator {
|
||||
Accelerator::from_str(self).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a closure that executes any JavaScript in the WebView context.
|
||||
pub fn use_global_shortcut(
|
||||
cx: &ScopeState,
|
||||
key: impl IntoKeyCode,
|
||||
modifiers: impl IntoModifersState,
|
||||
accelerator: impl IntoAccelerator,
|
||||
handler: impl FnMut() + 'static,
|
||||
) -> &Result<ShortcutHandle, ShortcutRegistryError> {
|
||||
let desktop = use_window(cx);
|
||||
cx.use_hook(move || {
|
||||
let desktop = desktop.clone();
|
||||
|
||||
let id = desktop.create_shortcut(
|
||||
key.into_key_code(),
|
||||
modifiers.into_modifiers_state(),
|
||||
handler,
|
||||
);
|
||||
let id = desktop.create_shortcut(accelerator.accelerator(), handler);
|
||||
|
||||
Ok(ShortcutHandle {
|
||||
desktop,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::desktop_context::EventData;
|
||||
use crate::protocol;
|
||||
use crate::{desktop_context::UserWindowEvent, Config};
|
||||
@@ -13,7 +11,7 @@ pub fn build(
|
||||
cfg: &mut Config,
|
||||
event_loop: &EventLoopWindowTarget<UserWindowEvent>,
|
||||
proxy: EventLoopProxy<UserWindowEvent>,
|
||||
) -> (Rc<WebView>, WebContext) {
|
||||
) -> (WebView, WebContext) {
|
||||
let builder = cfg.window.clone();
|
||||
let window = builder.build(event_loop).unwrap();
|
||||
let file_handler = cfg.file_drop_handler.take();
|
||||
@@ -64,6 +62,10 @@ pub fn build(
|
||||
webview = webview.with_browser_accelerator_keys(false);
|
||||
}
|
||||
|
||||
if let Some(color) = cfg.background_color {
|
||||
webview = webview.with_background_color(color);
|
||||
}
|
||||
|
||||
// These are commented out because wry is currently broken in wry
|
||||
// let mut web_context = WebContext::new(cfg.data_dir.clone());
|
||||
// .with_web_context(&mut web_context);
|
||||
@@ -92,5 +94,5 @@ pub fn build(
|
||||
webview = webview.with_devtools(true);
|
||||
}
|
||||
|
||||
(Rc::new(webview.build().unwrap()), web_context)
|
||||
(webview.build().unwrap(), web_context)
|
||||
}
|
||||
@@ -5,29 +5,26 @@ authors = ["Jonathan Kelley, @dementhos"]
|
||||
edition = "2021"
|
||||
description = "TUI-based renderer for Dioxus"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
documentation = "https://dioxuslabs.com"
|
||||
homepage = "https://dioxuslabs.com/docs/0.3/guide/en/getting_started/tui.html"
|
||||
keywords = ["dom", "ui", "gui", "react", "terminal"]
|
||||
license = "MIT/Apache-2.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus = { path = "../dioxus", version = "^0.3.0" }
|
||||
dioxus-core = { path = "../core", version = "^0.3.0", features = ["serialize"] }
|
||||
dioxus-html = { path = "../html", version = "^0.3.0" }
|
||||
dioxus-native-core = { path = "../native-core", version = "^0.2.0", features = ["dioxus"] }
|
||||
dioxus-native-core-macro = { path = "../native-core-macro", version = "^0.3.0" }
|
||||
dioxus-hot-reload = { path = "../hot-reload", optional = true }
|
||||
rink = { path = "../rink" }
|
||||
dioxus = { workspace = true }
|
||||
dioxus-core = { workspace = true, features = ["serialize"] }
|
||||
dioxus-html = { workspace = true }
|
||||
dioxus-native-core = { workspace = true, features = ["dioxus"] }
|
||||
dioxus-native-core-macro = { workspace = true }
|
||||
dioxus-hot-reload = { workspace = true, optional = true }
|
||||
rink = { workspace = true }
|
||||
|
||||
crossterm = "0.23.0"
|
||||
tokio = { version = "1.15.0", features = ["full"] }
|
||||
crossterm = "0.26.0"
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
futures = "0.3.19"
|
||||
taffy = "0.3.12"
|
||||
|
||||
[dev-dependencies]
|
||||
dioxus = { path = "../dioxus" }
|
||||
dioxus = { workspace = true }
|
||||
tokio = { version = "1" }
|
||||
criterion = "0.3.5"
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_html::FormData;
|
||||
use dioxus_tui::prelude::*;
|
||||
use dioxus_tui::Config;
|
||||
|
||||
fn main() {
|
||||
dioxus_tui::launch_cfg(app, Config::new());
|
||||
@@ -18,8 +17,8 @@ fn app(cx: Scope) -> Element {
|
||||
align_items: "center",
|
||||
justify_content: "center",
|
||||
|
||||
Input{
|
||||
oninput: |data: FormData| if &data.value == "good"{
|
||||
input {
|
||||
oninput: |data| if &data.value == "good"{
|
||||
bg_green.set(true);
|
||||
} else{
|
||||
bg_green.set(false);
|
||||
@@ -30,8 +29,8 @@ fn app(cx: Scope) -> Element {
|
||||
height: "10%",
|
||||
checked: "true",
|
||||
}
|
||||
Input{
|
||||
oninput: |data: FormData| if &data.value == "hello world"{
|
||||
input {
|
||||
oninput: |data| if &data.value == "hello world"{
|
||||
bg_green.set(true);
|
||||
} else{
|
||||
bg_green.set(false);
|
||||
@@ -40,8 +39,8 @@ fn app(cx: Scope) -> Element {
|
||||
height: "10%",
|
||||
maxlength: "11",
|
||||
}
|
||||
Input{
|
||||
oninput: |data: FormData| {
|
||||
input {
|
||||
oninput: |data| {
|
||||
if (data.value.parse::<f32>().unwrap() - 40.0).abs() < 5.0 {
|
||||
bg_green.set(true);
|
||||
} else{
|
||||
@@ -54,8 +53,8 @@ fn app(cx: Scope) -> Element {
|
||||
min: "20",
|
||||
max: "80",
|
||||
}
|
||||
Input{
|
||||
oninput: |data: FormData| {
|
||||
input {
|
||||
oninput: |data| {
|
||||
if data.value == "10"{
|
||||
bg_green.set(true);
|
||||
} else{
|
||||
@@ -67,8 +66,8 @@ fn app(cx: Scope) -> Element {
|
||||
height: "10%",
|
||||
maxlength: "4",
|
||||
}
|
||||
Input{
|
||||
oninput: |data: FormData| {
|
||||
input {
|
||||
oninput: |data| {
|
||||
if data.value == "hello world"{
|
||||
bg_green.set(true);
|
||||
} else{
|
||||
@@ -80,8 +79,10 @@ fn app(cx: Scope) -> Element {
|
||||
height: "10%",
|
||||
maxlength: "11",
|
||||
}
|
||||
Input{
|
||||
onclick: |_: FormData| bg_green.set(true),
|
||||
input {
|
||||
oninput: |_| {
|
||||
bg_green.set(true)
|
||||
},
|
||||
r#type: "button",
|
||||
value: "green",
|
||||
width: "50%",
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
mod element;
|
||||
pub mod prelude;
|
||||
pub mod widgets;
|
||||
|
||||
use std::{
|
||||
any::Any,
|
||||
@@ -10,7 +8,6 @@ use std::{
|
||||
};
|
||||
|
||||
use dioxus_core::{Component, ElementId, VirtualDom};
|
||||
use dioxus_html::EventData;
|
||||
use dioxus_native_core::dioxus::{DioxusState, NodeImmutableDioxusExt};
|
||||
use dioxus_native_core::prelude::*;
|
||||
|
||||
@@ -48,7 +45,7 @@ pub fn launch_cfg_with_props<Props: 'static>(app: Component<Props>, props: Props
|
||||
let mut dioxus_state = dioxus_state.write().unwrap();
|
||||
|
||||
// Find any mount events
|
||||
let mounted = dbg!(find_mount_events(&muts));
|
||||
let mounted = find_mount_events(&muts);
|
||||
|
||||
dioxus_state.apply_mutations(&mut rdom, muts);
|
||||
|
||||
@@ -119,13 +116,14 @@ impl Driver for DioxusRenderer {
|
||||
rdom: &Arc<RwLock<RealDom>>,
|
||||
id: NodeId,
|
||||
event: &str,
|
||||
value: Rc<EventData>,
|
||||
value: Rc<rink::EventData>,
|
||||
bubbles: bool,
|
||||
) {
|
||||
let id = { rdom.read().unwrap().get(id).unwrap().mounted_id() };
|
||||
if let Some(id) = id {
|
||||
let inner_value = value.deref().clone();
|
||||
self.vdom
|
||||
.handle_event(event, value.deref().clone().into_any(), id, bubbles);
|
||||
.handle_event(event, inner_value.into_any(), id, bubbles);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
pub use crate::widgets::*;
|
||||
pub use rink::Config;
|
||||
@@ -1,59 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_elements::input_data::keyboard_types::Key;
|
||||
use dioxus_html as dioxus_elements;
|
||||
use dioxus_html::FormData;
|
||||
|
||||
#[derive(Props)]
|
||||
pub(crate) struct ButtonProps<'a> {
|
||||
#[props(!optional)]
|
||||
raw_onclick: Option<&'a EventHandler<'a, FormData>>,
|
||||
#[props(!optional)]
|
||||
value: Option<&'a str>,
|
||||
#[props(!optional)]
|
||||
width: Option<&'a str>,
|
||||
#[props(!optional)]
|
||||
height: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub(crate) fn Button<'a>(cx: Scope<'a, ButtonProps>) -> Element<'a> {
|
||||
let state = use_state(cx, || false);
|
||||
let width = cx.props.width.unwrap_or("1px");
|
||||
let height = cx.props.height.unwrap_or("1px");
|
||||
|
||||
let single_char = width == "1px" || height == "1px";
|
||||
let text = if let Some(v) = cx.props.value { v } else { "" };
|
||||
let border_style = if single_char { "none" } else { "solid" };
|
||||
let update = || {
|
||||
let new_state = !state.get();
|
||||
if let Some(callback) = cx.props.raw_onclick {
|
||||
callback.call(FormData {
|
||||
value: text.to_string(),
|
||||
values: HashMap::new(),
|
||||
files: None,
|
||||
});
|
||||
}
|
||||
state.set(new_state);
|
||||
};
|
||||
render! {
|
||||
div{
|
||||
width: "{width}",
|
||||
height: "{height}",
|
||||
border_style: "{border_style}",
|
||||
flex_direction: "row",
|
||||
align_items: "center",
|
||||
justify_content: "center",
|
||||
onclick: move |_| {
|
||||
update();
|
||||
},
|
||||
onkeydown: move |evt|{
|
||||
if !evt.is_auto_repeating() && match evt.key(){ Key::Character(c) if c == " " =>true, Key::Enter=>true, _=>false } {
|
||||
update();
|
||||
}
|
||||
},
|
||||
"{text}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_elements::input_data::keyboard_types::Key;
|
||||
use dioxus_html as dioxus_elements;
|
||||
use dioxus_html::FormData;
|
||||
|
||||
#[derive(Props)]
|
||||
pub(crate) struct CheckBoxProps<'a> {
|
||||
#[props(!optional)]
|
||||
raw_oninput: Option<&'a EventHandler<'a, FormData>>,
|
||||
#[props(!optional)]
|
||||
value: Option<&'a str>,
|
||||
#[props(!optional)]
|
||||
width: Option<&'a str>,
|
||||
#[props(!optional)]
|
||||
height: Option<&'a str>,
|
||||
#[props(!optional)]
|
||||
checked: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub(crate) fn CheckBox<'a>(cx: Scope<'a, CheckBoxProps>) -> Element<'a> {
|
||||
let state = use_state(cx, || cx.props.checked.filter(|&c| c == "true").is_some());
|
||||
let width = cx.props.width.unwrap_or("1px");
|
||||
let height = cx.props.height.unwrap_or("1px");
|
||||
|
||||
let single_char = width == "1px" && height == "1px";
|
||||
let text = if single_char {
|
||||
if *state.get() {
|
||||
"☑"
|
||||
} else {
|
||||
"☐"
|
||||
}
|
||||
} else if *state.get() {
|
||||
"✓"
|
||||
} else {
|
||||
" "
|
||||
};
|
||||
let border_style = if width == "1px" || height == "1px" {
|
||||
"none"
|
||||
} else {
|
||||
"solid"
|
||||
};
|
||||
let update = move || {
|
||||
let new_state = !state.get();
|
||||
if let Some(callback) = cx.props.raw_oninput {
|
||||
callback.call(FormData {
|
||||
value: if let Some(value) = &cx.props.value {
|
||||
if new_state {
|
||||
value.to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
} else {
|
||||
"on".to_string()
|
||||
},
|
||||
values: HashMap::new(),
|
||||
files: None,
|
||||
});
|
||||
}
|
||||
state.set(new_state);
|
||||
};
|
||||
render! {
|
||||
div {
|
||||
width: "{width}",
|
||||
height: "{height}",
|
||||
border_style: "{border_style}",
|
||||
align_items: "center",
|
||||
justify_content: "center",
|
||||
onclick: move |_| {
|
||||
update();
|
||||
},
|
||||
onkeydown: move |evt| {
|
||||
if !evt.is_auto_repeating() && match evt.key(){ Key::Character(c) if c == " " =>true, Key::Enter=>true, _=>false } {
|
||||
update();
|
||||
}
|
||||
},
|
||||
"{text}"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 313 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user