Merge branch 'master' into master

This commit is contained in:
Jonathan Kelley authored and GitHub committed 2023-07-14 15:36:28 -07:00
commit af49cb4ec6
313 files changed
+23988 -2334

No files matched your search

+2 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
/target
Cargo.lock
.DS_Store
.idea/
+6
View File
@@ -0,0 +1,6 @@
{
"Lua.diagnostics.globals": [
"plugin_logger",
"PLUGIN_DOWNLOADER"
]
}
+4778
View File
File diff suppressed because it is too large. Load diff
+98
View File
@@ -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"
+45
View File
@@ -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 }
+43
View File
@@ -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).
+50
View File
@@ -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())
}
+1
View File
@@ -0,0 +1 @@
book
+6
View File
@@ -0,0 +1,6 @@
[book]
authors = ["YuKun Liu"]
language = "en"
multilingual = false
src = "src"
title = "Dioxus Cli"
+18
View File
@@ -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)
+26
View File
@@ -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
```
+47
View File
@@ -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
```
+18
View File
@@ -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
```
+61
View File
@@ -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
```
+68
View File
@@ -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"
}
}
})
}
```
+208
View File
@@ -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/"
```
+39
View File
@@ -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/)
+22
View File
@@ -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
```
+21
View File
@@ -0,0 +1,21 @@
# Introduction
📦✨ **Dioxus-Cli** is a tool to help get dioxus projects off the ground.
![dioxus-logo](https://dioxuslabs.com/guide/images/dioxuslogo_full.png)
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)
+79
View File
@@ -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.
View File
Whitespace-only changes.
+18
View File
@@ -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
+25
View File
@@ -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;
})()
+47
View File
@@ -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 = []
+22
View File
@@ -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>
+725
View File
@@ -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, &copy_options) {
Ok(_) => {}
Err(_e) => {
log::warn!("Error copying dir: {}", _e);
}
}
for ignore in &ignore_files {
let ignore = ignore.strip_prefix(&config.asset_dir).unwrap();
let ignore = config.out_dir.join(ignore);
if ignore.is_file() {
std::fs::remove_file(ignore)?;
}
}
}
}
}
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, &copy_options) {
Ok(_) => {}
Err(e) => {
log::warn!("Error copying dir: {}", e);
}
}
for ignore in &ignore_files {
let ignore = ignore.strip_prefix(&config.asset_dir).unwrap();
let ignore = config.out_dir.join(ignore);
if ignore.is_file() {
std::fs::remove_file(ignore)?;
}
}
}
}
}
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,
// )?)
// }
+94
View File
@@ -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()))
}
}
+210
View File
@@ -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);
}*/
+79
View File
@@ -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(())
}
}
+96
View File
@@ -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)
}
+33
View File
@@ -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(())
}
}
+63
View File
@@ -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(())
}
}
+76
View File
@@ -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
}
+92
View File
@@ -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"),
}
}
}
+39
View File
@@ -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(())
}
}
+100
View File
@@ -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(())
}
}
+64
View File
@@ -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(())
}
}
+138
View File
@@ -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);
}
+76
View File
@@ -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,
}
}
+282
View File
@@ -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
}
}
+80
View File
@@ -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)*)))
};
}
+25
View File
@@ -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;
+35
View File
@@ -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();
}
+71
View File
@@ -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) {}
}
+13
View File
@@ -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())
});
}
}
+85
View File
@@ -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)
});
}
}
+28
View File
@@ -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(())
});
}
}
+233
View File
@@ -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)
});
}
}
+18
View File
@@ -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");
}
});
}
}
+41
View File
@@ -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())
});
}
}
+331
View 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
}
}
+138
View File
@@ -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)
}
})
}
}
+70
View File
@@ -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;
};
}
}
})
}
+655
View File
@@ -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();
})
}
+127
View File
@@ -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()
);
}
}
+171
View File
@@ -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;
}
}
+349
View File
@@ -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(())
}
+4
View File
@@ -0,0 +1,4 @@
#[test]
fn ready() {
println!("Compiled successfully!")
}
+30
View File
@@ -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>
+48
View File
@@ -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>
+2 -3
View File
@@ -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"
+6 -4
View File
@@ -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()
}
+5
View File
@@ -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 -13
View File
@@ -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"
+2 -5
View File
@@ -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() {
+1 -1
View File
@@ -53,7 +53,7 @@ impl<T> Event<T> {
/// rsx! {
/// button {
/// onclick: move |evt: Event<MouseData>| {
/// evt.cancel_bubble();
/// evt.stop_propagation();
/// }
/// }
/// }
+1 -2
View File
@@ -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
View File
@@ -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###"
+64 -11
View File
@@ -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
View File
@@ -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);
}
+29
View File
@@ -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 {
+22 -21
View File
@@ -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,
}
})
+6 -9
View File
@@ -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 {
+26 -3
View File
@@ -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
View File
@@ -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];
});
}
}
+55
View File
@@ -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),
}
}
}
+59 -21
View File
@@ -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,
+6 -4
View File
@@ -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)
}
+11 -14
View File
@@ -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"
+15 -14
View File
@@ -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%",
+4 -6
View File
@@ -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);
}
}
-2
View File
@@ -1,2 +0,0 @@
pub use crate::widgets::*;
pub use rink::Config;
-59
View File
@@ -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